Skip to content

Commit b7beaf8

Browse files
authored
Merge pull request #2 from evander-wang/fix/02_06_meta_add_path_and_project_name
Fix/02 06 meta add path and project name
2 parents cba3428 + 90a06b5 commit b7beaf8

37 files changed

+855
-282
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,6 @@ TARGET_REPO_PATH=.
7676

7777
# Ollama base URL (without /v1 suffix)
7878
OLLAMA_BASE_URL=http://localhost:11434
79+
80+
ALLOWED_PROJECT_ROOTS=/path/to/project/root
81+
MCP_MODE=query

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ PROJECT.md
2323
.DS_Store
2424
.pypi_cache.json
2525
.omc
26+
openspec

README.md

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -513,17 +513,70 @@ The agent will incorporate the guidance from your reference documents when sugge
513513

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

516+
### MCP Dual Mode System (v0.0.60+)
517+
518+
The MCP server now supports two distinct modes with different capabilities and security profiles:
519+
520+
#### Query Mode (Production Recommended)
521+
**Read-only access** for safe codebase exploration and analysis.
522+
523+
**Available Tools:**
524+
- `list_projects` - List all indexed projects
525+
- `query_code_graph` - Natural language graph queries
526+
- `get_code_snippet` - Retrieve source code by qualified name
527+
- `list_directory` - Browse directory structure
528+
529+
**Use Cases:**
530+
- Production environments where code modification is not allowed
531+
- Code review and exploration
532+
- Documentation generation
533+
- Architecture analysis
534+
535+
#### Edit Mode (Development)
536+
**Full access** including file editing and database management.
537+
538+
**Additional Tools (beyond Query mode):**
539+
- `read_file` / `write_file` - File operations
540+
- `surgical_replace_code` - Precise code editing
541+
- `delete_project` - Remove projects from graph
542+
- `wipe_database` - Complete database reset (dangerous!)
543+
- `index_repository` - Build/update knowledge graph
544+
545+
**Use Cases:**
546+
- Local development environments
547+
- Code refactoring assistance
548+
- Automated code generation
549+
- Database maintenance
550+
516551
### Quick Setup
517552

553+
#### Query Mode (Recommended for Production)
554+
555+
```bash
556+
claude mcp add --transport stdio code-graph-rag \
557+
--env TARGET_REPO_PATH="$(pwd)" \
558+
--env MCP_MODE=query \
559+
--env CYPHER_PROVIDER=openai \
560+
--env CYPHER_MODEL=gpt-4 \
561+
--env CYPHER_API_KEY=your-api-key \
562+
-- uv run --directory /path/to/code-graph-rag code-graph-rag mcp-server
563+
```
564+
565+
#### Edit Mode (For Development)
566+
518567
```bash
519568
claude mcp add --transport stdio code-graph-rag \
520-
--env TARGET_REPO_PATH=/absolute/path/to/your/project \
569+
--env TARGET_REPO_PATH="$(pwd)" \
570+
--env MCP_MODE=edit \
571+
--env ALLOWED_PROJECT_ROOTS="$(pwd)" \
521572
--env CYPHER_PROVIDER=openai \
522573
--env CYPHER_MODEL=gpt-4 \
523574
--env CYPHER_API_KEY=your-api-key \
524575
-- uv run --directory /path/to/code-graph-rag code-graph-rag mcp-server
525576
```
526577

578+
**Important:** Always set `ALLOWED_PROJECT_ROOTS` in Edit mode to restrict file operations to specific directories.
579+
527580
### Available Tools
528581

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

544597
### Example Usage
545598

599+
#### Query Mode
546600
```
547-
> Index this repository
548601
> What functions call UserService.create_user?
602+
> Show me all classes that implement Repository
603+
> List all modules in the utils package
604+
> Get the source code for AuthService.login
605+
```
606+
607+
#### Edit Mode
608+
```
609+
> Index this repository
549610
> Update the login function to add rate limiting
611+
> Refactor this class to use dependency injection
612+
> Delete the deprecated project from the graph
550613
```
551614

552-
For detailed setup, see [Claude Code Setup Guide](docs/claude-code-setup.md).
615+
### Security Configuration
616+
617+
For Edit mode, always restrict access with `ALLOWED_PROJECT_ROOTS`:
618+
619+
```bash
620+
# Single project
621+
--env ALLOWED_PROJECT_ROOTS="/path/to/project"
622+
623+
# Multiple projects (comma-separated)
624+
--env ALLOWED_PROJECT_ROOTS="/path/to/project1,/path/to/project2"
625+
```
626+
627+
This ensures file operations cannot modify files outside the specified directories.
628+
629+
### Mode Selection Guide
630+
631+
| Scenario | Recommended Mode | Reasoning |
632+
|----------|-----------------|-----------|
633+
| Production code review | Query | Prevents accidental modifications |
634+
| Development work | Edit | Allows code generation and editing |
635+
| CI/CD pipelines | Query | Read-only analysis is sufficient |
636+
| Local experimentation | Edit | Full control for testing |
637+
| Multi-project analysis | Query | Safe exploration across projects |
638+
| Code refactoring | Edit | Requires write access |
639+
640+
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).
553641

554642
## 📊 Graph Schema
555643

@@ -560,20 +648,20 @@ The knowledge graph uses the following node types and relationships:
560648
<!-- SECTION:node_schemas -->
561649
| Label | Properties |
562650
|-----|----------|
563-
| Project | `{name: string}` |
564-
| Package | `{qualified_name: string, name: string, path: string}` |
565-
| Folder | `{path: string, name: string}` |
566-
| File | `{path: string, name: string, extension: string}` |
567-
| Module | `{qualified_name: string, name: string, path: string}` |
568-
| Class | `{qualified_name: string, name: string, decorators: list[string]}` |
569-
| Function | `{qualified_name: string, name: string, decorators: list[string]}` |
570-
| Method | `{qualified_name: string, name: string, decorators: list[string]}` |
571-
| Interface | `{qualified_name: string, name: string}` |
572-
| Enum | `{qualified_name: string, name: string}` |
573-
| Type | `{qualified_name: string, name: string}` |
574-
| Union | `{qualified_name: string, name: string}` |
575-
| ModuleInterface | `{qualified_name: string, name: string, path: string}` |
576-
| ModuleImplementation | `{qualified_name: string, name: string, path: string, implements_module: string}` |
651+
| Project | `{name: string, absolute_path: string, project_name: string}` |
652+
| Package | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
653+
| Folder | `{path: string, name: string, absolute_path: string, project_name: string}` |
654+
| File | `{path: string, name: string, extension: string, absolute_path: string, project_name: string}` |
655+
| Module | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
656+
| Class | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string, decorators: list[string]}` |
657+
| Function | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string, decorators: list[string]}` |
658+
| Method | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string, decorators: list[string]}` |
659+
| Interface | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
660+
| Enum | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
661+
| Type | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
662+
| Union | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
663+
| ModuleInterface | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string}` |
664+
| ModuleImplementation | `{qualified_name: string, name: string, path: string, absolute_path: string, project_name: string, implements_module: string}` |
577665
| ExternalPackage | `{name: string, version_spec: string}` |
578666
<!-- /SECTION:node_schemas -->
579667

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

744+
### MCP Server Configuration
745+
- `MCP_MODE`: MCP server operation mode - `query` (read-only) or `edit` (full access). Default: `edit`. **Recommended: Use `query` mode for production environments.**
746+
- `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`
747+
656748
### Custom Ignore Patterns
657749

658750
You can specify additional directories to exclude by creating a `.cgrignore` file in your repository root:

codebase_rag/config.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from dotenv import load_dotenv
88
from loguru import logger
9-
from pydantic import Field
9+
from pydantic import Field, field_validator
1010
from pydantic_settings import BaseSettings, SettingsConfigDict
1111

1212
from . import constants as cs
@@ -17,6 +17,19 @@
1717
load_dotenv()
1818

1919

20+
def _parse_frozenset_of_strings(value: str | frozenset[str] | None) -> frozenset[Path]:
21+
if value is None:
22+
return frozenset()
23+
if isinstance(value, frozenset):
24+
return frozenset(Path(path) for path in value)
25+
if isinstance(value, str):
26+
if value.strip():
27+
return frozenset(
28+
Path(path.strip()) for path in value.split(",") if path.strip()
29+
)
30+
return frozenset()
31+
32+
2033
class ApiKeyInfoEntry(TypedDict):
2134
env_var: str
2235
url: str
@@ -171,7 +184,21 @@ def ollama_endpoint(self) -> str:
171184
return f"{self.OLLAMA_BASE_URL.rstrip('/')}/v1"
172185

173186
TARGET_REPO_PATH: str = "."
187+
ALLOWED_PROJECT_ROOTS: str = ""
174188
SHELL_COMMAND_TIMEOUT: int = 30
189+
MCP_MODE: str = "edit"
190+
191+
@field_validator("MCP_MODE")
192+
@classmethod
193+
def _validate_mcp_mode(cls, v: str) -> str:
194+
if v not in ("query", "edit"):
195+
raise ValueError("MCP_MODE must be 'query' or 'edit'")
196+
return v
197+
198+
@property
199+
def allowed_project_roots_set(self) -> frozenset[Path]:
200+
return _parse_frozenset_of_strings(self.ALLOWED_PROJECT_ROOTS)
201+
175202
SHELL_COMMAND_ALLOWLIST: frozenset[str] = frozenset(
176203
{
177204
"ls",

codebase_rag/constants.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ class GoogleProviderType(StrEnum):
181181
KEY_VERSION_SPEC = "version_spec"
182182
KEY_PREFIX = "prefix"
183183
KEY_PROJECT_NAME = "project_name"
184+
KEY_ABSOLUTE_PATH = "absolute_path"
185+
EXTERNAL_PROJECT_NAME = "__external__"
184186
KEY_IS_EXTERNAL = "is_external"
185187

186188
ERR_SUBSTR_ALREADY_EXISTS = "already exists"
@@ -419,11 +421,10 @@ class RelationshipType(StrEnum):
419421

420422
CYPHER_QUERY_EMBEDDINGS = """
421423
MATCH (m:Module)-[:DEFINES]->(n)
422-
WHERE (n:Function OR n:Method)
423-
AND m.qualified_name STARTS WITH $project_name + '.'
424+
WHERE n.project_name = $project_name
424425
RETURN id(n) AS node_id, n.qualified_name AS qualified_name,
425426
n.start_line AS start_line, n.end_line AS end_line,
426-
m.path AS path
427+
n.path AS path
427428
"""
428429

429430

codebase_rag/cypher_queries.py

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,47 +13,58 @@
1313

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

1920
CYPHER_EXAMPLE_CONTENT_BY_PATH = f"""MATCH (n)
2021
WHERE n.path IS NOT NULL AND n.path STARTS WITH 'workflows'
21-
RETURN n.name AS name, n.path AS path, labels(n) AS type
22+
RETURN n.name AS name, n.path AS relative_path, n.absolute_path AS absolute_path,
23+
n.project_name AS project_name, labels(n) AS type
2224
LIMIT {CYPHER_DEFAULT_LIMIT}"""
2325

2426
CYPHER_EXAMPLE_KEYWORD_SEARCH = f"""MATCH (n)
2527
WHERE toLower(n.name) CONTAINS 'database' OR (n.qualified_name IS NOT NULL AND toLower(n.qualified_name) CONTAINS 'database')
26-
RETURN n.name AS name, n.qualified_name AS qualified_name, labels(n) AS type
28+
RETURN n.name AS name, n.qualified_name AS qualified_name, labels(n) AS type,
29+
n.path AS relative_path, n.absolute_path AS absolute_path, n.project_name AS project_name
2730
LIMIT {CYPHER_DEFAULT_LIMIT}"""
2831

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

3236
CYPHER_EXAMPLE_README = f"""MATCH (f:File)
3337
WHERE toLower(f.name) CONTAINS 'readme'
34-
RETURN f.path AS path, f.name AS name, labels(f) AS type
38+
RETURN f.path AS relative_path, f.absolute_path AS absolute_path, f.project_name AS project_name,
39+
f.name AS name, labels(f) AS type
3540
LIMIT {CYPHER_DEFAULT_LIMIT}"""
3641

3742
CYPHER_EXAMPLE_PYTHON_FILES = f"""MATCH (f:File)
3843
WHERE f.extension = '.py'
39-
RETURN f.path AS path, f.name AS name, labels(f) AS type
44+
RETURN f.path AS relative_path, f.absolute_path AS absolute_path, f.project_name AS project_name,
45+
f.name AS name, labels(f) AS type
4046
LIMIT {CYPHER_DEFAULT_LIMIT}"""
4147

4248
CYPHER_EXAMPLE_TASKS = f"""MATCH (n:Function|Method)
4349
WHERE 'task' IN n.decorators
44-
RETURN n.qualified_name AS qualified_name, n.name AS name, labels(n) AS type
50+
RETURN n.qualified_name AS qualified_name, n.name AS name, labels(n) AS type,
51+
n.path AS relative_path, n.absolute_path AS absolute_path, n.project_name AS project_name
4552
LIMIT {CYPHER_DEFAULT_LIMIT}"""
4653

4754
CYPHER_EXAMPLE_FILES_IN_FOLDER = f"""MATCH (f:File)
4855
WHERE f.path STARTS WITH 'services'
49-
RETURN f.path AS path, f.name AS name, labels(f) AS type
56+
RETURN f.path AS relative_path, f.absolute_path AS absolute_path, f.project_name AS project_name,
57+
f.name AS name, labels(f) AS type
5058
LIMIT {CYPHER_DEFAULT_LIMIT}"""
5159

52-
CYPHER_EXAMPLE_LIMIT_ONE = """MATCH (f:File) RETURN f.path as path, f.name as name, labels(f) as type LIMIT 1"""
60+
CYPHER_EXAMPLE_LIMIT_ONE = """MATCH (f:File)
61+
RETURN f.path AS relative_path, f.absolute_path AS absolute_path, f.project_name AS project_name,
62+
f.name as name, labels(f) as type LIMIT 1"""
5363

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

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

7283
CYPHER_GET_FUNCTION_SOURCE_LOCATION = """
73-
MATCH (m:Module)-[:DEFINES]->(n)
84+
MATCH (n)
7485
WHERE id(n) = $node_id
7586
RETURN n.qualified_name AS qualified_name, n.start_line AS start_line,
76-
n.end_line AS end_line, m.path AS path
87+
n.end_line AS end_line, n.path AS relative_path,
88+
n.absolute_path AS absolute_path, n.project_name AS project_name
7789
"""
7890

7991
CYPHER_FIND_BY_QUALIFIED_NAME = """
8092
MATCH (n) WHERE n.qualified_name = $qn
81-
OPTIONAL MATCH (m:Module)-[*]-(n)
82-
RETURN n.name AS name, n.start_line AS start, n.end_line AS end, m.path AS path, n.docstring AS docstring
93+
RETURN n.name AS name, n.start_line AS start, n.end_line AS end,
94+
n.path AS relative_path, n.absolute_path AS absolute_path,
95+
n.project_name AS project_name, n.docstring AS docstring
8396
LIMIT 1
8497
"""
8598

@@ -94,7 +107,9 @@ def build_nodes_by_ids_query(node_ids: list[int]) -> str:
94107
MATCH (n)
95108
WHERE id(n) IN [{placeholders}]
96109
RETURN id(n) AS node_id, n.qualified_name AS qualified_name,
97-
labels(n) AS type, n.name AS name
110+
labels(n) AS type, n.name AS name,
111+
n.path AS relative_path, n.absolute_path AS absolute_path,
112+
n.project_name AS project_name
98113
ORDER BY n.qualified_name
99114
"""
100115

@@ -126,3 +141,11 @@ def build_merge_relationship_query(
126141
)
127142
query += CYPHER_SET_PROPS_RETURN_COUNT if has_props else CYPHER_RETURN_COUNT
128143
return query
144+
145+
146+
def build_project_name_indexes() -> list[str]:
147+
return [
148+
build_index_query("Function", "project_name"),
149+
build_index_query("Method", "project_name"),
150+
build_index_query("Class", "project_name"),
151+
]

codebase_rag/decorators.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
LoadableProtocol,
1414
PathValidatorProtocol,
1515
)
16+
from .utils.path_utils import validate_allowed_path
1617

1718

1819
def ensure_loaded[T](func: Callable[..., T]) -> Callable[..., T]:
@@ -70,10 +71,10 @@ async def wrapper(self: PathValidatorProtocol, *args, **kwargs) -> T:
7071
file_path=str(file_path_str), error_message=ex.ACCESS_DENIED
7172
)
7273
try:
73-
full_path = (self.project_root / file_path_str).resolve()
74-
project_root = self.project_root.resolve()
75-
full_path.relative_to(project_root)
76-
except (ValueError, RuntimeError):
74+
full_path = validate_allowed_path(
75+
file_path_str, self.project_root, self.allowed_roots
76+
)
77+
except PermissionError:
7778
return result_factory(
7879
file_path=file_path_str,
7980
error_message=ls.FILE_OUTSIDE_ROOT.format(action="access"),

0 commit comments

Comments
 (0)