Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fa334dc
feat: add absolute_path and project_name to all nodes
Feb 7, 2026
1636e07
merge from main
Feb 7, 2026
2a4bfcc
fix: update node schemas and queries - use n.path directly, add missi…
Feb 8, 2026
3f8e959
refactor: add PathInfo TypedDict for type-safe path handling
Feb 8, 2026
2a2a1bc
refactor: use platform-native path format for cross-platform compatib…
Feb 8, 2026
02de6a9
feat: add MCP mode configuration for query-only support - add MCP_MOD…
Feb 9, 2026
cad8aae
Chore: Resolve an import
Feb 9, 2026
a8455ac
fix: prefer absolute_path over relative_path in get_function_source_code
Feb 9, 2026
3d607f0
fix: use absolute() instead of resolve() to preserve symlinks
Feb 9, 2026
b91a9bf
refactor: optimize embedding query with exact project_name match and …
Feb 10, 2026
cdd95dc
fix: enforce read-only behavior in query mode at handler layer by add…
Feb 10, 2026
73c27dd
refactor: use as_posix() for consistent cross-platform path formattin…
Feb 10, 2026
d50af9e
fix: add path validation to paginated read_file and use as_posix for …
Feb 10, 2026
9983470
docs: add MCP_MODE configuration example and move read_file to base t…
Feb 10, 2026
697eb01
fix: standardize path handling to use Path objects consistently and f…
Feb 10, 2026
d9a3467
fix: add ALLOWED_PROJECT_ROOTS support for read operations with valid…
Feb 10, 2026
edcb485
refactor: change allowed_project_roots_set return type from frozenset…
Feb 11, 2026
df87c96
refactor: remove redundant project_name field from Project node creation
Feb 11, 2026
6d6bff0
refactor: eliminate code duplication in MCPToolsRegistry.read_file() …
Feb 11, 2026
2894b26
fix: use validate_allowed_path in FileEditor.replace_code_block() for…
Feb 11, 2026
b23a868
refactor: add explicit fallback return to _parse_frozenset_of_strings…
Feb 11, 2026
90a06b5
refactor: use centralized WRITE_QUERY_MODE_BLOCKED constant in FileWr…
Feb 11, 2026
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,6 @@ TARGET_REPO_PATH=.

# Ollama base URL (without /v1 suffix)
OLLAMA_BASE_URL=http://localhost:11434

ALLOWED_PROJECT_ROOTS=/path/to/project/root
MCP_MODE=query
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ PROJECT.md
.DS_Store
.pypi_cache.json
.omc
openspec
126 changes: 109 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,17 +513,70 @@ The agent will incorporate the guidance from your reference documents when sugge

Code-Graph-RAG can run as an MCP (Model Context Protocol) server, enabling seamless integration with Claude Code and other MCP clients.

### MCP Dual Mode System (v0.0.60+)

The MCP server now supports two distinct modes with different capabilities and security profiles:

#### Query Mode (Production Recommended)
**Read-only access** for safe codebase exploration and analysis.

**Available Tools:**
- `list_projects` - List all indexed projects
- `query_code_graph` - Natural language graph queries
- `get_code_snippet` - Retrieve source code by qualified name
- `list_directory` - Browse directory structure

**Use Cases:**
- Production environments where code modification is not allowed
- Code review and exploration
- Documentation generation
- Architecture analysis

#### Edit Mode (Development)
**Full access** including file editing and database management.

**Additional Tools (beyond Query mode):**
- `read_file` / `write_file` - File operations
- `surgical_replace_code` - Precise code editing
- `delete_project` - Remove projects from graph
- `wipe_database` - Complete database reset (dangerous!)
- `index_repository` - Build/update knowledge graph

**Use Cases:**
- Local development environments
- Code refactoring assistance
- Automated code generation
- Database maintenance

### Quick Setup

#### Query Mode (Recommended for Production)

```bash
claude mcp add --transport stdio code-graph-rag \
--env TARGET_REPO_PATH="$(pwd)" \
--env MCP_MODE=query \
--env CYPHER_PROVIDER=openai \
--env CYPHER_MODEL=gpt-4 \
--env CYPHER_API_KEY=your-api-key \
-- uv run --directory /path/to/code-graph-rag code-graph-rag mcp-server
```

#### Edit Mode (For Development)

```bash
claude mcp add --transport stdio code-graph-rag \
--env TARGET_REPO_PATH=/absolute/path/to/your/project \
--env TARGET_REPO_PATH="$(pwd)" \
--env MCP_MODE=edit \
--env ALLOWED_PROJECT_ROOTS="$(pwd)" \
--env CYPHER_PROVIDER=openai \
--env CYPHER_MODEL=gpt-4 \
--env CYPHER_API_KEY=your-api-key \
-- uv run --directory /path/to/code-graph-rag code-graph-rag mcp-server
```

**Important:** Always set `ALLOWED_PROJECT_ROOTS` in Edit mode to restrict file operations to specific directories.

### Available Tools

<!-- SECTION:mcp_tools -->
Expand All @@ -543,13 +596,48 @@ claude mcp add --transport stdio code-graph-rag \

### Example Usage

#### Query Mode
```
> Index this repository
> What functions call UserService.create_user?
> Show me all classes that implement Repository
> List all modules in the utils package
> Get the source code for AuthService.login
```

#### Edit Mode
```
> Index this repository
> Update the login function to add rate limiting
> Refactor this class to use dependency injection
> Delete the deprecated project from the graph
```

For detailed setup, see [Claude Code Setup Guide](docs/claude-code-setup.md).
### Security Configuration

For Edit mode, always restrict access with `ALLOWED_PROJECT_ROOTS`:

```bash
# Single project
--env ALLOWED_PROJECT_ROOTS="/path/to/project"

# Multiple projects (comma-separated)
--env ALLOWED_PROJECT_ROOTS="/path/to/project1,/path/to/project2"
```

This ensures file operations cannot modify files outside the specified directories.

### Mode Selection Guide

| Scenario | Recommended Mode | Reasoning |
|----------|-----------------|-----------|
| Production code review | Query | Prevents accidental modifications |
| Development work | Edit | Allows code generation and editing |
| CI/CD pipelines | Query | Read-only analysis is sufficient |
| Local experimentation | Edit | Full control for testing |
| Multi-project analysis | Query | Safe exploration across projects |
| Code refactoring | Edit | Requires write access |

For detailed setup and configuration examples, see [Claude Code Setup Guide](docs/claude-code-setup.md) and [Security Best Practices](docs/security-best-practices.md).

## 📊 Graph Schema

Expand All @@ -560,20 +648,20 @@ The knowledge graph uses the following node types and relationships:
<!-- SECTION:node_schemas -->
| Label | Properties |
|-----|----------|
| Project | `{name: string}` |
| Package | `{qualified_name: string, name: string, path: string}` |
| Folder | `{path: string, name: string}` |
| File | `{path: string, name: string, extension: string}` |
| Module | `{qualified_name: string, name: string, path: string}` |
| Class | `{qualified_name: string, name: string, decorators: list[string]}` |
| Function | `{qualified_name: string, name: string, decorators: list[string]}` |
| Method | `{qualified_name: string, name: string, decorators: list[string]}` |
| Interface | `{qualified_name: string, name: string}` |
| Enum | `{qualified_name: string, name: string}` |
| Type | `{qualified_name: string, name: string}` |
| Union | `{qualified_name: string, name: string}` |
| ModuleInterface | `{qualified_name: string, name: string, path: string}` |
| ModuleImplementation | `{qualified_name: string, name: string, path: string, implements_module: string}` |
| Project | `{name: string, absolute_path: string, project_name: string}` |
| Package | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
| Folder | `{path: string, name: string, absolute_path: string, project_name: string}` |
| File | `{path: string, name: string, extension: string, absolute_path: string, project_name: string}` |
| Module | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
| Class | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string, decorators: list[string]}` |
| Function | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string, decorators: list[string]}` |
| Method | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string, decorators: list[string]}` |
| Interface | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
| Enum | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
| Type | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
| Union | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
| ModuleInterface | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
| ModuleImplementation | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string, implements_module: string}` |
| ExternalPackage | `{name: string, version_spec: string}` |
<!-- /SECTION:node_schemas -->

Expand Down Expand Up @@ -653,6 +741,10 @@ Configuration is managed through environment variables in `.env` file:
- `TARGET_REPO_PATH`: Default repository path (default: `.`)
- `LOCAL_MODEL_ENDPOINT`: Fallback endpoint for Ollama (default: `http://localhost:11434/v1`)

### MCP Server Configuration
- `MCP_MODE`: MCP server operation mode - `query` (read-only) or `edit` (full access). Default: `edit`. **Recommended: Use `query` mode for production environments.**
- `ALLOWED_PROJECT_ROOTS`: Comma-separated list of allowed project root paths for file operations in Edit mode. This is a critical security setting that restricts file read/write operations to specified directories. Example: `/path/to/project1,/path/to/project2`

### Custom Ignore Patterns

You can specify additional directories to exclude by creating a `.cgrignore` file in your repository root:
Expand Down
29 changes: 28 additions & 1 deletion codebase_rag/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from dotenv import load_dotenv
from loguru import logger
from pydantic import Field
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

from . import constants as cs
Expand All @@ -17,6 +17,19 @@
load_dotenv()


def _parse_frozenset_of_strings(value: str | frozenset[str] | None) -> frozenset[Path]:
if value is None:
return frozenset()
if isinstance(value, frozenset):
return frozenset(Path(path) for path in value)
if isinstance(value, str):
if value.strip():
return frozenset(
Path(path.strip()) for path in value.split(",") if path.strip()
)
return frozenset()


class ApiKeyInfoEntry(TypedDict):
env_var: str
url: str
Expand Down Expand Up @@ -171,7 +184,21 @@ def ollama_endpoint(self) -> str:
return f"{self.OLLAMA_BASE_URL.rstrip('/')}/v1"

TARGET_REPO_PATH: str = "."
ALLOWED_PROJECT_ROOTS: str = ""
SHELL_COMMAND_TIMEOUT: int = 30
MCP_MODE: str = "edit"

@field_validator("MCP_MODE")
@classmethod
def _validate_mcp_mode(cls, v: str) -> str:
if v not in ("query", "edit"):
raise ValueError("MCP_MODE must be 'query' or 'edit'")
return v

@property
def allowed_project_roots_set(self) -> frozenset[Path]:
return _parse_frozenset_of_strings(self.ALLOWED_PROJECT_ROOTS)

SHELL_COMMAND_ALLOWLIST: frozenset[str] = frozenset(
{
"ls",
Expand Down
7 changes: 4 additions & 3 deletions codebase_rag/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ class GoogleProviderType(StrEnum):
KEY_VERSION_SPEC = "version_spec"
KEY_PREFIX = "prefix"
KEY_PROJECT_NAME = "project_name"
KEY_ABSOLUTE_PATH = "absolute_path"
EXTERNAL_PROJECT_NAME = "__external__"
KEY_IS_EXTERNAL = "is_external"

ERR_SUBSTR_ALREADY_EXISTS = "already exists"
Expand Down Expand Up @@ -419,11 +421,10 @@ class RelationshipType(StrEnum):

CYPHER_QUERY_EMBEDDINGS = """
MATCH (m:Module)-[:DEFINES]->(n)
WHERE (n:Function OR n:Method)
AND m.qualified_name STARTS WITH $project_name + '.'
WHERE n.project_name = $project_name
RETURN id(n) AS node_id, n.qualified_name AS qualified_name,
n.start_line AS start_line, n.end_line AS end_line,
m.path AS path
n.path AS path
"""


Expand Down
53 changes: 38 additions & 15 deletions codebase_rag/cypher_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,58 @@

CYPHER_EXAMPLE_DECORATED_FUNCTIONS = f"""MATCH (n:Function|Method)
WHERE ANY(d IN n.decorators WHERE toLower(d) IN ['flow', 'task'])
RETURN n.name AS name, n.qualified_name AS qualified_name, labels(n) AS type
RETURN n.name AS name, n.qualified_name AS qualified_name, labels(n) AS type,
n.path AS relative_path, n.absolute_path AS absolute_path, n.project_name AS project_name
LIMIT {CYPHER_DEFAULT_LIMIT}"""

CYPHER_EXAMPLE_CONTENT_BY_PATH = f"""MATCH (n)
WHERE n.path IS NOT NULL AND n.path STARTS WITH 'workflows'
RETURN n.name AS name, n.path AS path, labels(n) AS type
RETURN n.name AS name, n.path AS relative_path, n.absolute_path AS absolute_path,
n.project_name AS project_name, labels(n) AS type
LIMIT {CYPHER_DEFAULT_LIMIT}"""

CYPHER_EXAMPLE_KEYWORD_SEARCH = f"""MATCH (n)
WHERE toLower(n.name) CONTAINS 'database' OR (n.qualified_name IS NOT NULL AND toLower(n.qualified_name) CONTAINS 'database')
RETURN n.name AS name, n.qualified_name AS qualified_name, labels(n) AS type
RETURN n.name AS name, n.qualified_name AS qualified_name, labels(n) AS type,
n.path AS relative_path, n.absolute_path AS absolute_path, n.project_name AS project_name
LIMIT {CYPHER_DEFAULT_LIMIT}"""

CYPHER_EXAMPLE_FIND_FILE = """MATCH (f:File) WHERE toLower(f.name) = 'readme.md' AND f.path = 'README.md'
RETURN f.path as path, f.name as name, labels(f) as type"""
RETURN f.path AS relative_path, f.absolute_path AS absolute_path, f.project_name AS project_name,
f.name as name, labels(f) as type"""

CYPHER_EXAMPLE_README = f"""MATCH (f:File)
WHERE toLower(f.name) CONTAINS 'readme'
RETURN f.path AS path, f.name AS name, labels(f) AS type
RETURN f.path AS relative_path, f.absolute_path AS absolute_path, f.project_name AS project_name,
f.name AS name, labels(f) AS type
LIMIT {CYPHER_DEFAULT_LIMIT}"""

CYPHER_EXAMPLE_PYTHON_FILES = f"""MATCH (f:File)
WHERE f.extension = '.py'
RETURN f.path AS path, f.name AS name, labels(f) AS type
RETURN f.path AS relative_path, f.absolute_path AS absolute_path, f.project_name AS project_name,
f.name AS name, labels(f) AS type
LIMIT {CYPHER_DEFAULT_LIMIT}"""

CYPHER_EXAMPLE_TASKS = f"""MATCH (n:Function|Method)
WHERE 'task' IN n.decorators
RETURN n.qualified_name AS qualified_name, n.name AS name, labels(n) AS type
RETURN n.qualified_name AS qualified_name, n.name AS name, labels(n) AS type,
n.path AS relative_path, n.absolute_path AS absolute_path, n.project_name AS project_name
LIMIT {CYPHER_DEFAULT_LIMIT}"""

CYPHER_EXAMPLE_FILES_IN_FOLDER = f"""MATCH (f:File)
WHERE f.path STARTS WITH 'services'
RETURN f.path AS path, f.name AS name, labels(f) AS type
RETURN f.path AS relative_path, f.absolute_path AS absolute_path, f.project_name AS project_name,
f.name AS name, labels(f) AS type
LIMIT {CYPHER_DEFAULT_LIMIT}"""

CYPHER_EXAMPLE_LIMIT_ONE = """MATCH (f:File) RETURN f.path as path, f.name as name, labels(f) as type LIMIT 1"""
CYPHER_EXAMPLE_LIMIT_ONE = """MATCH (f:File)
RETURN f.path AS relative_path, f.absolute_path AS absolute_path, f.project_name AS project_name,
f.name as name, labels(f) as type LIMIT 1"""

CYPHER_EXAMPLE_CLASS_METHODS = f"""MATCH (c:Class)-[:DEFINES_METHOD]->(m:Method)
WHERE c.qualified_name ENDS WITH '.UserService'
RETURN m.name AS name, m.qualified_name AS qualified_name, labels(m) AS type
RETURN m.name AS name, m.qualified_name AS qualified_name, labels(m) AS type,
m.path AS relative_path, m.absolute_path AS absolute_path, m.project_name AS project_name
LIMIT {CYPHER_DEFAULT_LIMIT}"""

CYPHER_EXPORT_NODES = """
Expand All @@ -70,16 +81,18 @@
CYPHER_SET_PROPS_RETURN_COUNT = "SET r += row.props\nRETURN count(r) as created"

CYPHER_GET_FUNCTION_SOURCE_LOCATION = """
MATCH (m:Module)-[:DEFINES]->(n)
MATCH (n)
WHERE id(n) = $node_id
RETURN n.qualified_name AS qualified_name, n.start_line AS start_line,
n.end_line AS end_line, m.path AS path
n.end_line AS end_line, n.path AS relative_path,
n.absolute_path AS absolute_path, n.project_name AS project_name
"""

CYPHER_FIND_BY_QUALIFIED_NAME = """
MATCH (n) WHERE n.qualified_name = $qn
OPTIONAL MATCH (m:Module)-[*]-(n)
RETURN n.name AS name, n.start_line AS start, n.end_line AS end, m.path AS path, n.docstring AS docstring
RETURN n.name AS name, n.start_line AS start, n.end_line AS end,
n.path AS relative_path, n.absolute_path AS absolute_path,
n.project_name AS project_name, n.docstring AS docstring
LIMIT 1
"""

Expand All @@ -94,7 +107,9 @@ def build_nodes_by_ids_query(node_ids: list[int]) -> str:
MATCH (n)
WHERE id(n) IN [{placeholders}]
RETURN id(n) AS node_id, n.qualified_name AS qualified_name,
labels(n) AS type, n.name AS name
labels(n) AS type, n.name AS name,
n.path AS relative_path, n.absolute_path AS absolute_path,
n.project_name AS project_name
ORDER BY n.qualified_name
"""

Expand Down Expand Up @@ -126,3 +141,11 @@ def build_merge_relationship_query(
)
query += CYPHER_SET_PROPS_RETURN_COUNT if has_props else CYPHER_RETURN_COUNT
return query


def build_project_name_indexes() -> list[str]:
return [
build_index_query("Function", "project_name"),
build_index_query("Method", "project_name"),
build_index_query("Class", "project_name"),
]
9 changes: 5 additions & 4 deletions codebase_rag/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
LoadableProtocol,
PathValidatorProtocol,
)
from .utils.path_utils import validate_allowed_path


def ensure_loaded[T](func: Callable[..., T]) -> Callable[..., T]:
Expand Down Expand Up @@ -70,10 +71,10 @@ async def wrapper(self: PathValidatorProtocol, *args, **kwargs) -> T:
file_path=str(file_path_str), error_message=ex.ACCESS_DENIED
)
try:
full_path = (self.project_root / file_path_str).resolve()
project_root = self.project_root.resolve()
full_path.relative_to(project_root)
except (ValueError, RuntimeError):
full_path = validate_allowed_path(
file_path_str, self.project_root, self.allowed_roots
)
except PermissionError:
return result_factory(
file_path=file_path_str,
error_message=ls.FILE_OUTSIDE_ROOT.format(action="access"),
Expand Down
Loading
Loading