Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
133 changes: 125 additions & 8 deletions codebase_rag/mcp/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,58 @@ def __init__(

# Build tool registry - single source of truth for all tool metadata
self._tools: dict[str, ToolMetadata] = {
"list_projects": ToolMetadata(
name="list_projects",
description="List all indexed projects in the knowledge graph database. "
"Returns a list of project names that have been indexed.",
input_schema={
"type": "object",
"properties": {},
"required": [],
},
handler=self.list_projects,
returns_json=True,
),
"delete_project": ToolMetadata(
name="delete_project",
description="Delete a specific project from the knowledge graph database. "
"This removes all nodes associated with the project while preserving other projects. "
"Use list_projects first to see available projects.",
input_schema={
"type": "object",
"properties": {
"project_name": {
"type": "string",
"description": "Name of the project to delete (e.g., 'my-project')",
}
},
"required": ["project_name"],
},
handler=self.delete_project,
returns_json=True,
),
"wipe_database": ToolMetadata(
name="wipe_database",
description="WARNING: Completely wipe the entire database, removing ALL indexed projects. "
"This cannot be undone. Use delete_project for removing individual projects.",
input_schema={
"type": "object",
"properties": {
"confirm": {
"type": "boolean",
"description": "Must be true to confirm the wipe operation",
}
},
"required": ["confirm"],
},
handler=self.wipe_database,
returns_json=False,
),
"index_repository": ToolMetadata(
name="index_repository",
description="Parse and ingest the repository into the Memgraph knowledge graph. "
"This builds a comprehensive graph of functions, classes, dependencies, and relationships.",
"This builds a comprehensive graph of functions, classes, dependencies, and relationships. "
"Note: This now preserves other projects - only the current project is re-indexed.",
input_schema={
"type": "object",
"properties": {},
Expand Down Expand Up @@ -216,26 +264,95 @@ def __init__(
),
}

async def list_projects(self) -> dict[str, Any]:
"""List all indexed projects in the knowledge graph database.

Returns:
Dictionary with list of project names
"""
logger.info("[MCP] Listing all projects...")
try:
projects = self.ingestor.list_projects()
return {
"projects": projects,
"count": len(projects),
}
except Exception as e:
logger.error(f"[MCP] Error listing projects: {e}")
return {"error": str(e), "projects": [], "count": 0}

async def delete_project(self, project_name: str) -> dict[str, Any]:
"""Delete a specific project from the knowledge graph database.

Args:
project_name: Name of the project to delete

Returns:
Dictionary with deletion status and count of deleted nodes
"""
logger.info(f"[MCP] Deleting project: {project_name}")
try:
# Verify project exists
projects = self.ingestor.list_projects()
if project_name not in projects:
return {
"success": False,
"error": f"Project '{project_name}' not found. Available projects: {projects}",
"deleted_nodes": 0,
}

deleted_count = self.ingestor.delete_project(project_name)
return {
"success": True,
"project": project_name,
"deleted_nodes": deleted_count,
"message": f"Successfully deleted project '{project_name}' ({deleted_count} nodes removed).",
}
except Exception as e:
logger.error(f"[MCP] Error deleting project: {e}")
return {"success": False, "error": str(e), "deleted_nodes": 0}

async def wipe_database(self, confirm: bool) -> str:
"""Completely wipe the entire database.

Args:
confirm: Must be True to proceed with the wipe

Returns:
Status message
"""
if not confirm:
return "Database wipe cancelled. Set confirm=true to proceed."

logger.warning("[MCP] Wiping entire database!")
try:
self.ingestor.clean_database()
return "Database completely wiped. All projects have been removed."
except Exception as e:
logger.error(f"[MCP] Error wiping database: {e}")
return f"Error wiping database: {str(e)}"

async def index_repository(self) -> str:
"""Parse and ingest the repository into the Memgraph knowledge graph.

This tool analyzes the codebase using Tree-sitter parsers and builds
a comprehensive knowledge graph with functions, classes, dependencies,
and relationships.

Note: This clears all existing data in the database before indexing.
Only one repository can be indexed at a time.
Note: This now only clears data for the current project, preserving other projects.

Returns:
Success message with indexing statistics
"""
logger.info(f"[MCP] Indexing repository at: {self.project_root}")
project_name = Path(self.project_root).name

try:
# Clear existing data to ensure clean state for the new repository
logger.info("[MCP] Clearing existing database to avoid conflicts...")
self.ingestor.clean_database()
logger.info("[MCP] Database cleared. Starting fresh indexing...")
# Delete only the current project's data (preserves other projects)
logger.info(f"[MCP] Clearing existing data for project '{project_name}'...")
deleted_count = self.ingestor.delete_project(project_name)
if deleted_count > 0:
logger.info(f"[MCP] Removed {deleted_count} existing nodes for '{project_name}'.")

updater = GraphUpdater(
ingestor=self.ingestor,
Expand All @@ -245,7 +362,7 @@ async def index_repository(self) -> str:
)
updater.run()

return f"Successfully indexed repository at {self.project_root}. Knowledge graph has been updated (previous data cleared)."
return f"Successfully indexed repository at {self.project_root}. Project '{project_name}' has been updated."
except Exception as e:
logger.error(f"[MCP] Error indexing repository: {e}")
return f"Error indexing repository: {str(e)}"
Expand Down
50 changes: 50 additions & 0 deletions codebase_rag/services/graph_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,60 @@ def _execute_batch_with_return(
cursor.close()

def clean_database(self) -> None:
"""Wipe the entire database. Use with caution."""
logger.info("--- Cleaning database... ---")
self._execute_query("MATCH (n) DETACH DELETE n;")
logger.info("--- Database cleaned. ---")

def list_projects(self) -> list[str]:
"""List all indexed projects in the database.

Returns:
List of project names
"""
result = self.fetch_all("MATCH (p:Project) RETURN p.name AS name ORDER BY p.name")
return [r["name"] for r in result]

def delete_project(self, project_name: str) -> int:
"""Delete all nodes associated with a specific project.

This removes the Project node and all nodes whose qualified_name
starts with the project name prefix, preserving other projects.

Args:
project_name: Name of the project to delete

Returns:
Number of nodes deleted
"""
logger.info(f"--- Deleting project: {project_name} ---")

# First, count nodes to be deleted
count_result = self.fetch_all(
"""
MATCH (n)
WHERE n.qualified_name STARTS WITH $prefix
OR (n:Project AND n.name = $project_name)
RETURN count(n) AS count
""",
{"prefix": f"{project_name}.", "project_name": project_name},
)
node_count = count_result[0]["count"] if count_result else 0

# Delete all nodes with qualified_name starting with project name
self._execute_query(
"""
MATCH (n)
WHERE n.qualified_name STARTS WITH $prefix
OR (n:Project AND n.name = $project_name)
DETACH DELETE n
""",
{"prefix": f"{project_name}.", "project_name": project_name},
)

logger.info(f"--- Project {project_name} deleted. {node_count} nodes removed. ---")
return node_count

def ensure_constraints(self) -> None:
logger.info("Ensuring constraints...")
for label, prop in self.unique_constraints.items():
Expand Down