Skip to content
Closed
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
4 changes: 3 additions & 1 deletion codemcp/git_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,5 +288,7 @@ async def get_current_commit_hash(directory: str, short: bool = True) -> str | N
return str(result.stdout.strip())
return None
except Exception as e:
logging.warning(f"Exception when getting current commit hash: {e!s}", exc_info=True)
logging.warning(
f"Exception when getting current commit hash: {e!s}", exc_info=True
)
return None
116 changes: 81 additions & 35 deletions codemcp/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os
import re
from pathlib import Path
from typing import List, Optional
from typing import List, Optional, Tuple

import click
import pathspec
Expand All @@ -15,6 +15,7 @@
from starlette.routing import Mount

from .common import normalize_file_path
from .git_query import get_current_commit_hash
from .tools.chmod import chmod
from .tools.edit_file import edit_file_content
from .tools.glob import MAX_RESULTS, glob_files
Expand All @@ -27,34 +28,37 @@
from .tools.think import think
from .tools.user_prompt import user_prompt as user_prompt_tool
from .tools.write_file import write_file_content
from .git_query import get_current_commit_hash

# Initialize FastMCP server
mcp = FastMCP("codemcp")


# Helper function to append the current commit hash to a result string
async def append_commit_hash(result: str, path: str | None) -> str:
"""Append the current Git commit hash to the result string.
# Helper function to get the current commit hash and append it to a result string
async def append_commit_hash(result: str, path: str | None) -> Tuple[str, str | None]:
"""Get the current Git commit hash and append it to the result string.

Args:
result: The original result string to append to
path: Path to the Git repository (if available)

Returns:
The result string with the commit hash appended
A tuple containing:
- The result string with the commit hash appended
- The current commit hash if available, None otherwise
"""
current_hash = None

if path is None:
return result
return result, None

try:
current_hash = await get_current_commit_hash(path)
if current_hash:
return f"{result}\n\nCurrent commit hash: {current_hash}"
return f"{result}\n\nCurrent commit hash: {current_hash}", current_hash
except Exception as e:
logging.warning(f"Failed to get current commit hash: {e}", exc_info=True)

return result
return result, current_hash


# NB: If you edit this, also edit codemcp/tools/init_project.py
Expand Down Expand Up @@ -82,6 +86,7 @@ async def codemcp(
| None = None, # Whether to reuse the chat ID from the HEAD commit
thought: str | None = None, # Added for Think tool
mode: str | None = None, # Added for Chmod tool
commit_hash: str | None = None, # Added for Git commit hash tracking
) -> str:
# NOTE: Do NOT add more documentation to this docblock when you add a new
# tool, documentation for tools should go in codemcp/tools/init_project.py.
Expand Down Expand Up @@ -110,8 +115,8 @@ async def codemcp(
try:
# Define expected parameters for each subtool
expected_params = {
"ReadFile": {"path", "offset", "limit", "chat_id"},
"WriteFile": {"path", "content", "description", "chat_id"},
"ReadFile": {"path", "offset", "limit", "chat_id", "commit_hash"},
"WriteFile": {"path", "content", "description", "chat_id", "commit_hash"},
"EditFile": {
"path",
"old_string",
Expand All @@ -120,21 +125,22 @@ async def codemcp(
"old_str",
"new_str",
"chat_id",
"commit_hash",
},
"LS": {"path", "chat_id"},
"LS": {"path", "chat_id", "commit_hash"},
"InitProject": {
"path",
"user_prompt",
"subject_line",
"reuse_head_chat_id",
}, # chat_id is not expected for InitProject as it's generated there
"UserPrompt": {"user_prompt", "chat_id"},
"RunCommand": {"path", "command", "arguments", "chat_id"},
"Grep": {"pattern", "path", "include", "chat_id"},
"Glob": {"pattern", "path", "limit", "offset", "chat_id"},
"RM": {"path", "description", "chat_id"},
"Think": {"thought", "chat_id"},
"Chmod": {"path", "mode", "chat_id"},
"UserPrompt": {"user_prompt", "chat_id", "commit_hash"},
"RunCommand": {"path", "command", "arguments", "chat_id", "commit_hash"},
"Grep": {"pattern", "path", "include", "chat_id", "commit_hash"},
"Glob": {"pattern", "path", "limit", "offset", "chat_id", "commit_hash"},
"RM": {"path", "description", "chat_id", "commit_hash"},
"Think": {"thought", "chat_id", "commit_hash"},
"Chmod": {"path", "mode", "chat_id", "commit_hash"},
}

# Check if subtool exists
Expand Down Expand Up @@ -189,6 +195,8 @@ def normalize_newlines(s: object) -> object:
"thought": thought,
# Chmod tool parameter
"mode": mode,
# Git commit hash tracking
"commit_hash": commit_hash,
}.items()
if value is not None
}
Expand All @@ -213,7 +221,8 @@ def normalize_newlines(s: object) -> object:
normalized_path = normalize_file_path(path)

result = await read_file_content(normalized_path, offset, limit, chat_id)
return await append_commit_hash(result, normalized_path)
result, new_commit_hash = await append_commit_hash(result, normalized_path)
return result

if subtool == "WriteFile":
if path is None:
Expand All @@ -237,7 +246,8 @@ def normalize_newlines(s: object) -> object:
result = await write_file_content(
normalized_path, content_str, description, chat_id
)
return await append_commit_hash(result, normalized_path)
result, new_commit_hash = await append_commit_hash(result, normalized_path)
return result

if subtool == "EditFile":
if path is None:
Expand All @@ -262,7 +272,8 @@ def normalize_newlines(s: object) -> object:
result = await edit_file_content(
normalized_path, old_content, new_content, None, description, chat_id
)
return await append_commit_hash(result, normalized_path)
result, new_commit_hash = await append_commit_hash(result, normalized_path)
return result

if subtool == "LS":
if path is None:
Expand All @@ -272,7 +283,8 @@ def normalize_newlines(s: object) -> object:
normalized_path = normalize_file_path(path)

result = await ls_directory(normalized_path, chat_id)
return await append_commit_hash(result, normalized_path)
result, new_commit_hash = await append_commit_hash(result, normalized_path)
return result

if subtool == "InitProject":
if path is None:
Expand Down Expand Up @@ -317,12 +329,14 @@ def normalize_newlines(s: object) -> object:
if isinstance(arguments, str) or arguments is None
else " ".join(arguments)
)
return await run_command(
result = await run_command(
normalized_path,
command,
args_str,
chat_id,
)
result, new_commit_hash = await append_commit_hash(result, normalized_path)
return result

if subtool == "Grep":
if pattern is None:
Expand All @@ -335,11 +349,17 @@ def normalize_newlines(s: object) -> object:
normalized_path = normalize_file_path(path)

try:
result = await grep_files(pattern, normalized_path, include, chat_id)
return result.get(
grep_result = await grep_files(
pattern, normalized_path, include, chat_id
)
result_string = grep_result.get(
"resultForAssistant",
f"Found {result.get('numFiles', 0)} file(s)",
f"Found {grep_result.get('numFiles', 0)} file(s)",
)
result, new_commit_hash = await append_commit_hash(
result_string, normalized_path
)
return result
except Exception as e:
# Log the error but don't suppress it - let it propagate
logging.error(f"Exception in grep subtool: {e!s}", exc_info=True)
Expand All @@ -356,17 +376,21 @@ def normalize_newlines(s: object) -> object:
normalized_path = normalize_file_path(path)

try:
result = await glob_files(
glob_result = await glob_files(
pattern,
normalized_path,
limit=limit if limit is not None else MAX_RESULTS,
offset=offset if offset is not None else 0,
chat_id=chat_id,
)
return result.get(
result_string = glob_result.get(
"resultForAssistant",
f"Found {result.get('numFiles', 0)} file(s)",
f"Found {glob_result.get('numFiles', 0)} file(s)",
)
result, new_commit_hash = await append_commit_hash(
result_string, normalized_path
)
return result
except Exception as e:
# Log the error but don't suppress it - let it propagate
logging.error(f"Exception in glob subtool: {e!s}", exc_info=True)
Expand All @@ -376,7 +400,14 @@ def normalize_newlines(s: object) -> object:
if user_prompt is None:
raise ValueError("user_prompt is required for UserPrompt subtool")

return await user_prompt_tool(user_prompt, chat_id)
result = await user_prompt_tool(user_prompt, chat_id)
# UserPrompt doesn't need a path, but we might have one in the provided parameters
if path:
normalized_path = normalize_file_path(path)
result, new_commit_hash = await append_commit_hash(
result, normalized_path
)
return result

if subtool == "RM":
if path is None:
Expand All @@ -389,13 +420,22 @@ def normalize_newlines(s: object) -> object:

if chat_id is None:
raise ValueError("chat_id is required for RM subtool")
return await rm_file(normalized_path, description, chat_id)
result = await rm_file(normalized_path, description, chat_id)
result, new_commit_hash = await append_commit_hash(result, normalized_path)
return result

if subtool == "Think":
if thought is None:
raise ValueError("thought is required for Think subtool")

return await think(thought, chat_id)
result = await think(thought, chat_id)
# Think doesn't need a path, but we might have one in the provided parameters
if path:
normalized_path = normalize_file_path(path)
result, new_commit_hash = await append_commit_hash(
result, normalized_path
)
return result

if subtool == "Chmod":
if path is None:
Expand All @@ -416,8 +456,14 @@ def normalize_newlines(s: object) -> object:
from typing import Literal, cast

chmod_mode = cast(Literal["a+x", "a-x"], mode)
result = await chmod(normalized_path, chmod_mode, chat_id)
return result.get("resultForAssistant", "Chmod operation completed")
chmod_result = await chmod(normalized_path, chmod_mode, chat_id)
result_string = chmod_result.get(
"resultForAssistant", "Chmod operation completed"
)
result, new_commit_hash = await append_commit_hash(
result_string, normalized_path
)
return result
except Exception:
logging.error("Exception", exc_info=True)
raise
Expand Down
9 changes: 6 additions & 3 deletions codemcp/tools/init_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,8 +456,8 @@ async def init_project(

## Chmod chat_id path mode

Changes file permissions using chmod. Unlike standard chmod, this tool only supports
a+x (add executable permission) and a-x (remove executable permission), because these
Changes file permissions using chmod. Unlike standard chmod, this tool only supports
a+x (add executable permission) and a-x (remove executable permission), because these
are the only bits that git knows how to track.

Args:
Expand Down Expand Up @@ -486,9 +486,12 @@ async def init_project(
mode: The chmod mode to apply (a+x or a-x) for Chmod subtool
chat_id: A unique ID to identify the chat session (required for all tools EXCEPT InitProject)

# Chat ID
# Chat ID and Git tracking
This chat has been assigned a chat ID: {chat_id}
When you use any tool, you MUST always include this chat ID as the chat_id parameter.

# Git Commit Hash
This project uses Git commit hashes to track changes across conversations. After each operation that modifies files, the current Git commit hash will be reported. The commit hash represents the current state of the repository.
"""

# Combine system prompt, global prompt
Expand Down
Loading