Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,9 @@ claude mcp add --transport stdio graph-code \
| `read_file` | Read the contents of a file from the project. Supports pagination for large files. |
| `write_file` | Write content to a file, creating it if it doesn't exist. |
| `list_directory` | List contents of a directory in the project. |
| `list_projects` | List all indexed projects in the knowledge graph database. Returns a list of project names that have been indexed. |
| `delete_project` | 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. |
| `wipe_database` | WARNING: Completely wipe the entire database, removing ALL indexed projects. This cannot be undone. Use delete_project for removing individual projects. |
<!-- /SECTION:mcp_tools -->

### Example Usage
Expand Down
6 changes: 6 additions & 0 deletions codebase_rag/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2301,6 +2301,9 @@ class MCPToolName(StrEnum):
READ_FILE = "read_file"
WRITE_FILE = "write_file"
LIST_DIRECTORY = "list_directory"
LIST_PROJECTS = "list_projects"
DELETE_PROJECT = "delete_project"
WIPE_DATABASE = "wipe_database"


# (H) MCP environment variables
Expand All @@ -2315,6 +2318,7 @@ class MCPSchemaType(StrEnum):
OBJECT = "object"
STRING = "string"
INTEGER = "integer"
BOOLEAN = "boolean"


# (H) MCP schema fields
Expand All @@ -2337,6 +2341,8 @@ class MCPParamName(StrEnum):
LIMIT = "limit"
CONTENT = "content"
DIRECTORY_PATH = "directory_path"
PROJECT_NAME = "project_name"
CONFIRM = "confirm"


# (H) MCP server constants
Expand Down
21 changes: 21 additions & 0 deletions codebase_rag/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@
MG_BATCH_PARAMS_TRUNCATED = " Params (first 10 of {count}): {params}..."
MG_CLEANING_DB = "--- Cleaning database... ---"
MG_DB_CLEANED = "--- Database cleaned. ---"
MG_LISTING_PROJECTS = "--- Listing indexed projects... ---"
MG_DELETING_PROJECT = "--- Deleting project: {name} ---"
MG_PROJECT_DELETED = "--- Project {name} deleted. ---"
MG_ENSURING_CONSTRAINTS = "Ensuring constraints..."
MG_CONSTRAINTS_DONE = "Constraints checked/created."
MG_NODE_BUFFER_FLUSH = (
Expand Down Expand Up @@ -580,6 +583,24 @@
MCP_ERROR_WRITE = "[MCP] Error writing file: {error}"
MCP_LIST_DIR = "[MCP] list_directory: {path}"
MCP_ERROR_LIST_DIR = "[MCP] Error listing directory: {error}"
MCP_LIST_PROJECTS = "[MCP] Listing all indexed projects..."
MCP_LIST_PROJECTS_RESULT = "[MCP] Found {count} indexed projects"
MCP_ERROR_LIST_PROJECTS = "[MCP] Error listing projects: {error}"
MCP_DELETE_PROJECT = "[MCP] Deleting project: {name}"
MCP_DELETE_PROJECT_SUCCESS = "[MCP] Successfully deleted project: {name}"
MCP_DELETE_PROJECT_NOT_FOUND = "[MCP] Project not found: {name}"
MCP_ERROR_DELETE_PROJECT = "[MCP] Error deleting project: {error}"
MCP_WIPE_DB = "[MCP] Wiping entire database!"
MCP_WIPE_DB_CANCELLED = "[MCP] Database wipe cancelled (confirm=false)"
MCP_ERROR_WIPE_DB = "[MCP] Error wiping database: {error}"
MCP_CLEARING_PROJECT = "[MCP] Clearing existing data for project '{name}'..."
MCP_INDEX_PROJECT_SUCCESS = "[MCP] Successfully indexed project '{name}'"
MCP_SCOPE_VIOLATION = (
"[MCP] Scope violation: attempted to access '{target}' from project '{current}'"
)
MCP_WIPE_DISABLED = (
"[MCP] wipe_database is disabled in scoped mode. Use delete_project instead."
)

# (H) MCP server logs
MCP_SERVER_INFERRED_ROOT = "[GraphCode MCP] Using inferred project root: {path}"
Expand Down
122 changes: 118 additions & 4 deletions codebase_rag/mcp/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from codebase_rag.tools.file_writer import FileWriter, create_file_writer_tool
from codebase_rag.types_defs import (
CodeSnippetResultDict,
DeleteProjectResultDict,
ListProjectsResultDict,
MCPHandlerType,
MCPInputSchema,
MCPInputSchemaProperty,
Expand All @@ -37,10 +39,13 @@ def __init__(
project_root: str,
ingestor: MemgraphIngestor,
cypher_gen: CypherGenerator,
enforce_scope: bool = True,
) -> None:
self.project_root = project_root
self.ingestor = ingestor
self.cypher_gen = cypher_gen
self.enforce_scope = enforce_scope
self.current_project = Path(project_root).resolve().name

self.parsers, self.queries = load_parsers()

Expand Down Expand Up @@ -197,15 +202,58 @@ def __init__(
handler=self.list_directory,
returns_json=False,
),
cs.MCPToolName.LIST_PROJECTS: ToolMetadata(
name=cs.MCPToolName.LIST_PROJECTS,
description=td.MCP_TOOLS[cs.MCPToolName.LIST_PROJECTS],
input_schema=MCPInputSchema(
type=cs.MCPSchemaType.OBJECT,
properties={},
required=[],
),
handler=self.list_projects,
returns_json=True,
),
cs.MCPToolName.DELETE_PROJECT: ToolMetadata(
name=cs.MCPToolName.DELETE_PROJECT,
description=td.MCP_TOOLS[cs.MCPToolName.DELETE_PROJECT],
input_schema=MCPInputSchema(
type=cs.MCPSchemaType.OBJECT,
properties={
cs.MCPParamName.PROJECT_NAME: MCPInputSchemaProperty(
type=cs.MCPSchemaType.STRING,
description=td.MCP_PARAM_PROJECT_NAME,
)
},
required=[cs.MCPParamName.PROJECT_NAME],
),
handler=self.delete_project,
returns_json=True,
),
cs.MCPToolName.WIPE_DATABASE: ToolMetadata(
name=cs.MCPToolName.WIPE_DATABASE,
description=td.MCP_TOOLS[cs.MCPToolName.WIPE_DATABASE],
input_schema=MCPInputSchema(
type=cs.MCPSchemaType.OBJECT,
properties={
cs.MCPParamName.CONFIRM: MCPInputSchemaProperty(
type=cs.MCPSchemaType.BOOLEAN,
description=td.MCP_PARAM_CONFIRM,
)
},
required=[cs.MCPParamName.CONFIRM],
),
handler=self.wipe_database,
returns_json=False,
),
}

async def index_repository(self) -> str:
logger.info(lg.MCP_INDEXING_REPO.format(path=self.project_root))
project_name = Path(self.project_root).resolve().name

try:
logger.info(lg.MCP_CLEARING_DB)
self.ingestor.clean_database()
logger.info(lg.MCP_DB_CLEARED)
logger.info(lg.MCP_CLEARING_PROJECT.format(name=project_name))
self.ingestor.delete_project(project_name)

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

return cs.MCP_INDEX_SUCCESS.format(path=self.project_root)
return lg.MCP_INDEX_PROJECT_SUCCESS.format(name=project_name)
except Exception as e:
logger.error(lg.MCP_ERROR_INDEXING.format(error=e))
return cs.MCP_INDEX_ERROR.format(error=e)
Expand Down Expand Up @@ -339,6 +387,72 @@ async def list_directory(
logger.error(lg.MCP_ERROR_LIST_DIR.format(error=e))
return te.ERROR_WRAPPER.format(message=e)

async def list_projects(self) -> ListProjectsResultDict:
logger.info(lg.MCP_LIST_PROJECTS)
try:
all_projects = self.ingestor.list_projects()
if self.enforce_scope:
projects = [p for p in all_projects if p == self.current_project]
else:
projects = all_projects
logger.info(lg.MCP_LIST_PROJECTS_RESULT.format(count=len(projects)))
return ListProjectsResultDict(projects=projects, count=len(projects))
except Exception as e:
logger.error(lg.MCP_ERROR_LIST_PROJECTS.format(error=e))
return ListProjectsResultDict(error=str(e), projects=[], count=0)

async def delete_project(self, project_name: str) -> DeleteProjectResultDict:
logger.info(lg.MCP_DELETE_PROJECT.format(name=project_name))
try:
if self.enforce_scope and project_name != self.current_project:
logger.warning(
lg.MCP_SCOPE_VIOLATION.format(
target=project_name, current=self.current_project
)
)
return DeleteProjectResultDict(
success=False,
error=f"Scope violation: can only delete '{self.current_project}', "
f"not '{project_name}'.",
)
projects = self.ingestor.list_projects()
if project_name not in projects:
logger.warning(
lg.MCP_DELETE_PROJECT_NOT_FOUND.format(name=project_name)
)
return DeleteProjectResultDict(
success=False,
error=f"Project '{project_name}' not found.",
)
self.ingestor.delete_project(project_name)
logger.info(lg.MCP_DELETE_PROJECT_SUCCESS.format(name=project_name))
return DeleteProjectResultDict(
success=True,
project=project_name,
message=f"Successfully deleted project '{project_name}'.",
)
except Exception as e:
logger.error(lg.MCP_ERROR_DELETE_PROJECT.format(error=e))
return DeleteProjectResultDict(success=False, error=str(e))

async def wipe_database(self, confirm: bool) -> str:
if self.enforce_scope:
logger.warning(lg.MCP_WIPE_DISABLED)
return (
"wipe_database is disabled in scoped mode to protect other projects. "
f"Use delete_project('{self.current_project}') to remove this project only."
)
if not confirm:
logger.info(lg.MCP_WIPE_DB_CANCELLED)
return "Database wipe cancelled. Set confirm=true to proceed."
logger.warning(lg.MCP_WIPE_DB)
try:
self.ingestor.clean_database()
return "Database completely wiped. All projects have been removed."
except Exception as e:
logger.error(lg.MCP_ERROR_WIPE_DB.format(error=e))
return te.ERROR_WRAPPER.format(message=e)

def get_tool_schemas(self) -> list[MCPToolSchema]:
return [
MCPToolSchema(
Expand Down
20 changes: 20 additions & 0 deletions codebase_rag/services/graph_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,26 @@ def clean_database(self) -> None:
self._execute_query(CYPHER_DELETE_ALL)
logger.info(ls.MG_DB_CLEANED)

def list_projects(self) -> list[str]:
logger.info(ls.MG_LISTING_PROJECTS)
result = self.fetch_all(
"MATCH (p:Project) RETURN p.name AS name ORDER BY p.name"
)
return [str(r["name"]) for r in result if r.get("name")]

def delete_project(self, project_name: str) -> None:
logger.info(ls.MG_DELETING_PROJECT.format(name=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(ls.MG_PROJECT_DELETED.format(name=project_name))

def ensure_constraints(self) -> None:
logger.info(ls.MG_ENSURING_CONSTRAINTS)
for label, prop in NODE_UNIQUE_CONSTRAINTS.items():
Expand Down
21 changes: 21 additions & 0 deletions codebase_rag/tools/tool_descriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,22 @@ class AgenticToolName(StrEnum):

MCP_LIST_DIRECTORY = "List contents of a directory in the project."

MCP_LIST_PROJECTS = (
"List all indexed projects in the knowledge graph database. "
"Returns a list of project names that have been indexed."
)

MCP_DELETE_PROJECT = (
"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."
)

MCP_WIPE_DATABASE = (
"WARNING: Completely wipe the entire database, removing ALL indexed projects. "
"This cannot be undone. Use delete_project for removing individual projects."
)

MCP_PARAM_NATURAL_LANGUAGE_QUERY = "Your question in plain English about the codebase"
MCP_PARAM_QUALIFIED_NAME = (
"Fully qualified name (e.g., 'app.services.UserService.create_user')"
Expand All @@ -111,6 +127,8 @@ class AgenticToolName(StrEnum):
MCP_PARAM_LIMIT = "Maximum number of lines to read (optional)"
MCP_PARAM_CONTENT = "Content to write to the file"
MCP_PARAM_DIRECTORY_PATH = "Relative path to directory from project root (default: '.')"
MCP_PARAM_PROJECT_NAME = "Name of the project to delete (e.g., 'my-project')"
MCP_PARAM_CONFIRM = "Must be true to confirm the wipe operation"


MCP_TOOLS: dict[MCPToolName, str] = {
Expand All @@ -121,6 +139,9 @@ class AgenticToolName(StrEnum):
MCPToolName.READ_FILE: MCP_READ_FILE,
MCPToolName.WRITE_FILE: MCP_WRITE_FILE,
MCPToolName.LIST_DIRECTORY: MCP_LIST_DIRECTORY,
MCPToolName.LIST_PROJECTS: MCP_LIST_PROJECTS,
MCPToolName.DELETE_PROJECT: MCP_DELETE_PROJECT,
MCPToolName.WIPE_DATABASE: MCP_WIPE_DATABASE,
}

AGENTIC_TOOLS: dict[AgenticToolName, str] = {
Expand Down
21 changes: 20 additions & 1 deletion codebase_rag/types_defs.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,26 @@ class CodeSnippetResultDict(TypedDict, total=False):
error: str


MCPResultType = str | QueryResultDict | CodeSnippetResultDict
class ListProjectsResultDict(TypedDict, total=False):
projects: list[str]
count: int
error: str


class DeleteProjectResultDict(TypedDict, total=False):
success: bool
project: str
message: str
error: str


MCPResultType = (
str
| QueryResultDict
| CodeSnippetResultDict
| ListProjectsResultDict
| DeleteProjectResultDict
)
MCPHandlerType = Callable[..., Awaitable[MCPResultType]]


Expand Down