From ff7c72e55b95b82f2996bccda66f2359a7b76696 Mon Sep 17 00:00:00 2001 From: "Edward Z. Yang" Date: Mon, 5 May 2025 21:54:00 -0400 Subject: [PATCH 1/2] Update [ghstack-poisoned] --- codemcp/main.py | 320 ---------------------------------- codemcp/tools/chmod.py | 18 +- codemcp/tools/edit_file.py | 50 +++++- codemcp/tools/glob.py | 7 +- codemcp/tools/grep.py | 10 +- codemcp/tools/init_project.py | 14 +- codemcp/tools/ls.py | 5 +- codemcp/tools/mv.py | 16 +- codemcp/tools/read_file.py | 8 +- codemcp/tools/rm.py | 15 +- codemcp/tools/think.py | 6 +- codemcp/tools/write_file.py | 12 +- 12 files changed, 133 insertions(+), 348 deletions(-) diff --git a/codemcp/main.py b/codemcp/main.py index b4b2f47..173812a 100644 --- a/codemcp/main.py +++ b/codemcp/main.py @@ -14,330 +14,10 @@ from starlette.applications import Starlette from starlette.routing import Mount -from .tools.chmod import chmod -from .tools.edit_file import edit_file -from .tools.glob import glob -from .tools.grep import grep -from .tools.init_project import init_project -from .tools.ls import ls -from .tools.mv import mv -from .tools.read_file import read_file -from .tools.rm import rm -from .tools.run_command import run_command -from .tools.think import think -from .tools.write_file import write_file - # Initialize FastMCP server mcp = FastMCP("codemcp") -# NB: If you edit this, also edit codemcp/tools/init_project.py -@mcp.tool() -async def codemcp( - subtool: str, - *, - path: str | None = None, - content: str - | dict - | list - | None = None, # Allow any type, will be serialized to string if needed - old_string: str | None = None, - new_string: str | None = None, - offset: int | None = None, - limit: int | None = None, - description: str | None = None, - pattern: str | None = None, - include: str | None = None, - command: str | None = None, - arguments: str | None = None, - old_str: str | None = None, # Added because Claude often hallucinates this - new_str: str | None = None, # Added because Claude often hallucinates this - chat_id: str | None = None, # Added for chat identification - user_prompt: str | None = None, # Added for InitProject commit message - subject_line: str | None = None, # Added for InitProject commit message - reuse_head_chat_id: bool - | 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 - source_path: str | None = None, # Added for MV tool - target_path: str | None = None, # Added for MV 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. - # This includes documentation for new parameters! ONLY InitProject's - # parmaeters are documented here. - """If and only if the user explicitly asks you to initialize codemcp with - path, you should invoke this tool. This will return instructions which you should - IMMEDIATELY follow before continuing, in particular, it will explain other ways - you can invoke this tool. - - If the user indicates they want to "amend" or "continue working" on a PR, - you should set reuse_head_chat_id=True to continue using the same chat ID. - - Arguments: - subtool: The subtool to run (InitProject, ...) - path: The path to the file or directory to operate on - user_prompt: The user's original prompt verbatim, starting AFTER instructions to initialize codemcp (e.g., you should exclude "Initialize codemcp for PATH") - subject_line: A short subject line in Git conventional commit format (for InitProject) - reuse_head_chat_id: If True, reuse the chat ID from the HEAD commit instead of generating a new one (for InitProject) - ... (there are other arguments which will be documented when you InitProject) - """ - try: - # Define expected parameters for each subtool - expected_params = { - "ReadFile": {"path", "offset", "limit", "chat_id", "commit_hash"}, - "WriteFile": {"path", "content", "description", "chat_id", "commit_hash"}, - "EditFile": { - "path", - "old_string", - "new_string", - "description", - "old_str", - "new_str", - "chat_id", - "commit_hash", - }, - "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 - "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"}, - "MV": { - "source_path", - "target_path", - "description", - "chat_id", - "commit_hash", - }, - "Think": {"thought", "chat_id", "commit_hash"}, - "Chmod": {"path", "mode", "chat_id", "commit_hash"}, - } - - # Check if subtool exists - if subtool not in expected_params: - raise ValueError( - f"Unknown subtool: {subtool}. Available subtools: {', '.join(expected_params.keys())}" - ) - - # Get all provided non-None parameters - provided_params = { - param: value - for param, value in { - "path": path, - "content": content, - "old_string": old_string, - "new_string": new_string, - "offset": offset, - "limit": limit, - "description": description, - "pattern": pattern, - "include": include, - "command": command, - "arguments": arguments, - # Include backward compatibility parameters - "old_str": old_str, - "new_str": new_str, - # Chat ID for session identification - "chat_id": chat_id, - # InitProject commit message parameters - "user_prompt": user_prompt, - "subject_line": subject_line, - # Whether to reuse the chat ID from the HEAD commit - "reuse_head_chat_id": reuse_head_chat_id, - # Think tool parameter - "thought": thought, - # Chmod tool parameter - "mode": mode, - # MV tool parameters - "source_path": source_path, - "target_path": target_path, - # Git commit hash tracking - "commit_hash": commit_hash, - }.items() - if value is not None - } - - # Check for unexpected parameters - unexpected_params = set(provided_params.keys()) - expected_params[subtool] - if unexpected_params: - raise ValueError( - f"Unexpected parameters for {subtool} subtool: {', '.join(unexpected_params)}" - ) - - # Check for required chat_id for all tools except InitProject - if subtool != "InitProject" and chat_id is None: - raise ValueError(f"chat_id is required for {subtool} subtool") - - # Now handle each subtool with its expected parameters - if subtool == "ReadFile": - if path is None: - raise ValueError("path is required for ReadFile subtool") - - result = await read_file(**provided_params) - return result - - if subtool == "WriteFile": - if path is None: - raise ValueError("path is required for WriteFile subtool") - if description is None: - raise ValueError("description is required for WriteFile subtool") - if chat_id is None: - raise ValueError("chat_id is required for WriteFile subtool") - - result = await write_file(**provided_params) - return result - - if subtool == "EditFile": - if path is None: - raise ValueError("path is required for EditFile subtool") - if description is None: - raise ValueError("description is required for EditFile subtool") - if old_string is None and old_str is None: - # TODO: I want telemetry to tell me when this occurs. - raise ValueError( - "Either old_string or old_str is required for EditFile subtool (use empty string for new file creation)" - ) - if chat_id is None: - raise ValueError("chat_id is required for EditFile subtool") - - result = await edit_file(**provided_params) - return result - - if subtool == "LS": - if path is None: - raise ValueError("path is required for LS subtool") - - result = await ls(**provided_params) - return result - - if subtool == "InitProject": - if path is None: - raise ValueError("path is required for InitProject subtool") - if user_prompt is None: - raise ValueError("user_prompt is required for InitProject subtool") - if subject_line is None: - raise ValueError("subject_line is required for InitProject subtool") - - # Handle parameter naming differences with adapter pattern in the central point - if "path" in provided_params and "directory" not in provided_params: - provided_params["directory"] = provided_params.pop("path") - - # Ensure reuse_head_chat_id has a default value - if ( - "reuse_head_chat_id" not in provided_params - or provided_params["reuse_head_chat_id"] is None - ): - provided_params["reuse_head_chat_id"] = False - - return await init_project(**provided_params) - - if subtool == "RunCommand": - # When is something a command as opposed to a subtool? They are - # basically the same thing, but commands are always USER defined. - # This means we shove them all in RunCommand so they are guaranteed - # not to conflict with codemcp's subtools. - - if path is None: - raise ValueError("path is required for RunCommand subtool") - if command is None: - raise ValueError("command is required for RunCommand subtool") - if chat_id is None: - raise ValueError("chat_id is required for RunCommand subtool") - - # Handle parameter naming differences with adapter pattern in the central point - if "path" in provided_params and "project_dir" not in provided_params: - provided_params["project_dir"] = provided_params.pop("path") - - result = await run_command(**provided_params) - return result - - if subtool == "Grep": - if pattern is None: - raise ValueError("pattern is required for Grep subtool") - if path is None: - raise ValueError("path is required for Grep subtool") - - try: - result_string = await grep(**provided_params) - return result_string - except Exception as e: - logging.error(f"Error in Grep subtool: {e}", exc_info=True) - raise - - if subtool == "Glob": - if pattern is None: - raise ValueError("pattern is required for Glob subtool") - if path is None: - raise ValueError("path is required for Glob subtool") - - try: - result_string = await glob(**provided_params) - return result_string - except Exception as e: - logging.error(f"Error in Glob subtool: {e}", exc_info=True) - raise - - if subtool == "RM": - if path is None: - raise ValueError("path is required for RM subtool") - if description is None: - raise ValueError("description is required for RM subtool") - if chat_id is None: - raise ValueError("chat_id is required for RM subtool") - - result = await rm(**provided_params) - return result - - if subtool == "MV": - # Extract parameters specific to MV - source_path = provided_params.get("source_path") - target_path = provided_params.get("target_path") - - if source_path is None: - raise ValueError("source_path is required for MV subtool") - if target_path is None: - raise ValueError("target_path is required for MV subtool") - if description is None: - raise ValueError("description is required for MV subtool") - if chat_id is None: - raise ValueError("chat_id is required for MV subtool") - - result = await mv(**provided_params) - return result - - if subtool == "Think": - if thought is None: - raise ValueError("thought is required for Think subtool") - - result = await think(**provided_params) - return result - - if subtool == "Chmod": - if path is None: - raise ValueError("path is required for Chmod subtool") - if mode is None: - raise ValueError("mode is required for Chmod subtool") - if chat_id is None: - raise ValueError("chat_id is required for Chmod subtool") - - result_string = await chmod(**provided_params) - return result_string - - except Exception: - logging.error("Exception", exc_info=True) - raise - - # This should never be reached, but adding for type safety - return "Unknown subtool or operation" - - def get_files_respecting_gitignore(dir_path: Path, pattern: str = "**/*") -> List[Path]: """Get files in a directory respecting .gitignore rules in all subdirectories. diff --git a/codemcp/tools/chmod.py b/codemcp/tools/chmod.py index 6a5b5e5..821827b 100644 --- a/codemcp/tools/chmod.py +++ b/codemcp/tools/chmod.py @@ -6,6 +6,7 @@ from ..common import normalize_file_path from ..git import commit_changes +from ..main import mcp from ..shell import run_command from .commit_utils import append_commit_hash @@ -18,8 +19,8 @@ TOOL_NAME_FOR_PROMPT = "Chmod" DESCRIPTION = """ -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. Example: @@ -28,20 +29,27 @@ """ +@mcp.tool() async def chmod( path: str, mode: str, chat_id: str | None = None, commit_hash: str | None = None, ) -> str: - """Change file permissions using chmod. + """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: - path: The path to the file to change permissions for + path: The absolute path to the file to modify mode: The chmod mode to apply, only "a+x" and "a-x" are supported - chat_id: The unique ID of the current chat session + chat_id: The unique ID to identify the chat session commit_hash: Optional Git commit hash for version tracking + Example: + chmod a+x path/to/file # Makes a file executable by all users + chmod a-x path/to/file # Makes a file non-executable for all users + Returns: A formatted string with the chmod operation result """ diff --git a/codemcp/tools/edit_file.py b/codemcp/tools/edit_file.py index b4c6621..bb4a7b2 100644 --- a/codemcp/tools/edit_file.py +++ b/codemcp/tools/edit_file.py @@ -19,6 +19,7 @@ ) from ..git import commit_changes from ..line_endings import detect_line_endings +from ..main import mcp from .commit_utils import append_commit_hash # Set up logger @@ -744,6 +745,7 @@ def debug_string_comparison( return not content_same +@mcp.tool() async def edit_file( path: str, old_string: str | None = None, @@ -753,12 +755,50 @@ async def edit_file( chat_id: str | None = None, commit_hash: str | None = None, ) -> str: - """Edit a file by replacing old_string with new_string. + """This is a tool for editing files. For larger edits, use the WriteFile tool to overwrite files. + Provide a short description of the change. - If the old_string is not found in the file, attempts a fallback mechanism - where trailing whitespace is stripped from blank lines (lines with only whitespace) - before matching. This helps match files where the only difference is in trailing - whitespace on otherwise empty lines. + Before using this tool: + + 1. Use the ReadFile tool to understand the file's contents and context + + 2. Verify the directory path is correct (only applicable when creating new files): + - Use the LS tool to verify the parent directory exists and is the correct location + + To make a file edit, provide the following: + 1. path: The absolute path to the file to modify (must be absolute, not relative) + 2. old_string: The text to replace (must be unique within the file, and must match the file contents exactly, including all whitespace and indentation) + 3. new_string: The edited text to replace the old_string + + The tool will replace ONE occurrence of old_string with new_string in the specified file. + + CRITICAL REQUIREMENTS FOR USING THIS TOOL: + + 1. UNIQUENESS: The old_string MUST uniquely identify the specific instance you want to change. This means: + - Include AT LEAST 3-5 lines of context BEFORE the change point + - Include AT LEAST 3-5 lines of context AFTER the change point + - Include all whitespace, indentation, and surrounding code exactly as it appears in the file + + 2. SINGLE INSTANCE: This tool can only change ONE instance at a time. If you need to change multiple instances: + - Make separate calls to this tool for each instance + - Each call must uniquely identify its specific instance using extensive context + + 3. VERIFICATION: Before using this tool: + - Check how many instances of the target text exist in the file + - If multiple instances exist, gather enough context to uniquely identify each one + - Plan separate tool calls for each instance + + WARNING: If you do not follow these requirements: + - The tool will fail if old_string matches multiple locations + - The tool will fail if old_string doesn't match exactly (including whitespace) + - You may change the wrong instance if you don't include enough context + + When making edits: + - Ensure the edit results in idiomatic, correct code + - Do not leave the code in a broken state + - Always use absolute file paths (starting with /) + + Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each. Args: path: The absolute path to the file to edit diff --git a/codemcp/tools/glob.py b/codemcp/tools/glob.py index 201f355..6192c6e 100644 --- a/codemcp/tools/glob.py +++ b/codemcp/tools/glob.py @@ -7,6 +7,7 @@ from ..common import normalize_file_path from ..git import is_git_repository +from ..main import mcp from .commit_utils import append_commit_hash __all__ = [ @@ -39,6 +40,7 @@ def render_result_for_assistant(output: Dict[str, Any]) -> str: return result +@mcp.tool() async def glob( pattern: str, path: str, @@ -47,7 +49,10 @@ async def glob( chat_id: str | None = None, commit_hash: str | None = None, ) -> str: - """Find files matching a pattern. + """Fast file pattern matching tool that works with any codebase size + Supports glob patterns like "**/*.js" or "src/**/*.ts" + Returns matching file paths sorted by modification time + Use this tool when you need to find files by name patterns Args: pattern: The glob pattern to match files against diff --git a/codemcp/tools/grep.py b/codemcp/tools/grep.py index a759e24..28f89f6 100644 --- a/codemcp/tools/grep.py +++ b/codemcp/tools/grep.py @@ -7,6 +7,7 @@ from ..common import normalize_file_path from ..git import is_git_repository +from ..main import mcp from ..shell import run_command from .commit_utils import append_commit_hash @@ -156,6 +157,7 @@ def render_result_for_assistant(output: Dict[str, Any]) -> str: return result +@mcp.tool() async def grep( pattern: str, path: str | None = None, @@ -163,7 +165,13 @@ async def grep( chat_id: str | None = None, commit_hash: str | None = None, ) -> str: - """Search for a pattern in files within a directory or in a specific file. + """Searches for files containing a specified pattern (regular expression) using git grep. + Files with a match are returned, up to a maximum of 100 files. + Note that this tool only works inside git repositories. + + Example: + Grep "function.*hello" /path/to/repo # Find files containing functions with "hello" in their name + Grep "console\.log" /path/to/repo --include="*.js" # Find JS files with console.log statements Args: pattern: The regular expression pattern to search for diff --git a/codemcp/tools/init_project.py b/codemcp/tools/init_project.py index 0c25f6e..f48e204 100644 --- a/codemcp/tools/init_project.py +++ b/codemcp/tools/init_project.py @@ -10,6 +10,7 @@ from ..common import MAX_LINE_LENGTH, MAX_LINES_TO_READ, normalize_file_path from ..git import get_repository_root, is_git_repository +from ..main import mcp __all__ = [ "init_project", @@ -120,6 +121,7 @@ async def _generate_chat_id(directory: str, description: Optional[str] = None) - return f"0-{human_readable_part}" +@mcp.tool() async def init_project( directory: Optional[str] = None, user_prompt: str = "", @@ -127,13 +129,17 @@ async def init_project( reuse_head_chat_id: bool = False, path: Optional[str] = None, ) -> str: - """Initialize a project by reading the codemcp.toml TOML file and returning - a combined system prompt. Creates an empty commit with the user's prompt as the body - and a subject line in Git conventional commit format. + """If and only if the user explicitly asks you to initialize codemcp with + path, you should invoke this tool. This will return instructions which you should + IMMEDIATELY follow before continuing, in particular, it will explain other ways + you can invoke this tool. + + If the user indicates they want to "amend" or "continue working" on a PR, + you should set reuse_head_chat_id=True to continue using the same chat ID. Args: directory: The directory path containing the codemcp.toml file - user_prompt: The user's original prompt verbatim + user_prompt: The user's original prompt verbatim, starting AFTER instructions to initialize codemcp (e.g., you should exclude "Initialize codemcp for PATH") subject_line: A short subject line in Git conventional commit format reuse_head_chat_id: Whether to reuse the chat ID from the HEAD commit path: Alias for directory parameter (for backward compatibility) diff --git a/codemcp/tools/ls.py b/codemcp/tools/ls.py index 8fcee3a..b39c62d 100644 --- a/codemcp/tools/ls.py +++ b/codemcp/tools/ls.py @@ -7,6 +7,7 @@ from ..access import check_edit_permission from ..common import normalize_file_path from ..git import is_git_repository +from ..main import mcp from .commit_utils import append_commit_hash __all__ = [ @@ -23,10 +24,12 @@ TRUNCATED_MESSAGE = f"There are more than {MAX_FILES} files in the directory. Use more specific paths to explore nested directories. The first {MAX_FILES} files and directories are included below:\n\n" +@mcp.tool() async def ls( path: str, chat_id: str | None = None, commit_hash: str | None = None ) -> str: - """List the contents of a directory. + """Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. + You should generally prefer the Glob and Grep tools, if you know which directories to search. Args: path: The absolute path to the directory to list diff --git a/codemcp/tools/mv.py b/codemcp/tools/mv.py index 58c8807..1440bc0 100644 --- a/codemcp/tools/mv.py +++ b/codemcp/tools/mv.py @@ -6,6 +6,7 @@ from ..common import normalize_file_path from ..git import commit_changes, get_repository_root +from ..main import mcp from ..shell import run_command from .commit_utils import append_commit_hash @@ -14,6 +15,7 @@ ] +@mcp.tool() async def mv( source_path: str, target_path: str, @@ -21,13 +23,19 @@ async def mv( chat_id: str | None = None, commit_hash: str | None = None, ) -> str: - """Move a file using git mv. + """Moves a file using git mv and commits the change. + Provide a short description of why the file is being moved. + + Before using this tool: + 1. Ensure the source file exists and is tracked by git + 2. Ensure the target directory exists within the git repository + 3. Provide a meaningful description of why the file is being moved Args: - source_path: The path to the source file to move (can be absolute or relative to repository root) - target_path: The path to the target location (can be absolute or relative to repository root) + source_path: The path to the file to move (can be relative to the project root or absolute) + target_path: The destination path where the file should be moved to (can be relative to the project root or absolute) description: Short description of why the file is being moved - chat_id: The unique ID of the current chat session + chat_id: The unique ID to identify the chat session commit_hash: Optional Git commit hash for version tracking Returns: diff --git a/codemcp/tools/read_file.py b/codemcp/tools/read_file.py index 122a63d..3802118 100644 --- a/codemcp/tools/read_file.py +++ b/codemcp/tools/read_file.py @@ -10,6 +10,7 @@ normalize_file_path, ) from ..git_query import find_git_root +from ..main import mcp from ..rules import get_applicable_rules_content from .commit_utils import append_commit_hash @@ -18,6 +19,7 @@ ] +@mcp.tool() async def read_file( path: str, offset: int | None = None, @@ -25,7 +27,11 @@ async def read_file( chat_id: str | None = None, commit_hash: str | None = None, ) -> str: - """Read a file's content with optional offset and limit. + """Reads a file from the local filesystem. The path parameter must be an absolute path, not a relative path. + By default, it reads up to 1000 lines starting from the beginning of the file. You can optionally specify a + line offset and limit (especially handy for long files), but it's recommended to read the whole file by not + providing these parameters. Any lines longer than 1000 characters will be truncated. For image files, the + tool will display the image for you. Args: path: The absolute path to the file to read diff --git a/codemcp/tools/rm.py b/codemcp/tools/rm.py index e8f6d4b..d9239f7 100644 --- a/codemcp/tools/rm.py +++ b/codemcp/tools/rm.py @@ -8,6 +8,7 @@ from ..access import check_edit_permission from ..common import normalize_file_path from ..git import commit_changes, get_repository_root, is_git_repository +from ..main import mcp from ..shell import run_command from .commit_utils import append_commit_hash @@ -16,15 +17,21 @@ ] +@mcp.tool() async def rm( path: str, description: str, chat_id: str, commit_hash: Optional[str] = None ) -> str: - """Remove a file or directory. + """Removes a file using git rm and commits the change. + Provide a short description of why the file is being removed. + + Before using this tool: + 1. Ensure the file exists and is tracked by git + 2. Provide a meaningful description of why the file is being removed Args: - path: The absolute path to the file or directory to remove - description: Short description of the change - chat_id: The unique ID of the current chat session + path: The path to the file to remove (can be relative to the project root or absolute) + description: Short description of why the file is being removed + chat_id: The unique ID to identify the chat session commit_hash: Optional Git commit hash for version tracking Returns: diff --git a/codemcp/tools/think.py b/codemcp/tools/think.py index e90cc6d..b5f460b 100644 --- a/codemcp/tools/think.py +++ b/codemcp/tools/think.py @@ -2,15 +2,19 @@ import logging +from ..main import mcp + __all__ = [ "think", ] +@mcp.tool() async def think( thought: str, chat_id: str | None = None, commit_hash: str | None = None ) -> str: - """Use this tool to think about something without obtaining new information or changing the database. + """Use the tool to think about something. It will not obtain new information or change the database, + but just append the thought to the log. Use it when complex reasoning or some cache memory is needed. Args: thought: The thought to log diff --git a/codemcp/tools/write_file.py b/codemcp/tools/write_file.py index 5c12b4d..36d8fc7 100644 --- a/codemcp/tools/write_file.py +++ b/codemcp/tools/write_file.py @@ -13,6 +13,7 @@ ) from ..git import commit_changes from ..line_endings import detect_line_endings, detect_repo_line_endings +from ..main import mcp from .commit_utils import append_commit_hash __all__ = [ @@ -20,6 +21,7 @@ ] +@mcp.tool() async def write_file( path: str, content: str | dict | list | None = None, @@ -27,7 +29,15 @@ async def write_file( chat_id: str | None = None, commit_hash: str | None = None, ) -> str: - """Write content to a file. + """Write a file to the local filesystem. Overwrites the existing file if there is one. + Provide a short description of the change. + + Before using this tool: + + 1. Use the ReadFile tool to understand the file's contents and context + + 2. Directory Verification (only applicable when creating new files): + - Use the LS tool to verify the parent directory exists and is the correct location Args: path: The absolute path to the file to write From cdc5463aa6df76ad62490456c14abb57a585886d Mon Sep 17 00:00:00 2001 From: "Edward Z. Yang" Date: Mon, 5 May 2025 21:59:25 -0400 Subject: [PATCH 2/2] Update [ghstack-poisoned] --- codemcp/__init__.py | 3 +- codemcp/main.py | 138 +++++++++++++++++++++++++++++++++- codemcp/mcp.py | 10 +++ codemcp/tools/chmod.py | 2 +- codemcp/tools/edit_file.py | 2 +- codemcp/tools/glob.py | 2 +- codemcp/tools/grep.py | 4 +- codemcp/tools/init_project.py | 2 +- codemcp/tools/ls.py | 2 +- codemcp/tools/mv.py | 2 +- codemcp/tools/read_file.py | 2 +- codemcp/tools/rm.py | 2 +- codemcp/tools/think.py | 2 +- codemcp/tools/write_file.py | 2 +- 14 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 codemcp/mcp.py diff --git a/codemcp/__init__.py b/codemcp/__init__.py index cb97ba9..21f3f93 100644 --- a/codemcp/__init__.py +++ b/codemcp/__init__.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -from .main import cli, codemcp, configure_logging, mcp, run +from .main import cli, codemcp, configure_logging, run +from .mcp import mcp from .shell import get_subprocess_env, run_command __all__ = [ diff --git a/codemcp/main.py b/codemcp/main.py index 173812a..79564ba 100644 --- a/codemcp/main.py +++ b/codemcp/main.py @@ -10,12 +10,144 @@ import pathspec import uvicorn from fastapi.middleware.cors import CORSMiddleware -from mcp.server.fastmcp import FastMCP from starlette.applications import Starlette from starlette.routing import Mount -# Initialize FastMCP server -mcp = FastMCP("codemcp") +from .mcp import mcp +from .tools.chmod import chmod +from .tools.edit_file import edit_file +from .tools.glob import glob +from .tools.grep import grep +from .tools.init_project import init_project +from .tools.ls import ls +from .tools.mv import mv +from .tools.read_file import read_file +from .tools.rm import rm +from .tools.run_command import run_command +from .tools.think import think +from .tools.write_file import write_file + + +# For backward compatibility - this tool forwards to the individual tools +@mcp.tool() +async def codemcp( + subtool: str, + *, + path: str | None = None, + content: str + | dict + | list + | None = None, # Allow any type, will be serialized to string if needed + old_string: str | None = None, + new_string: str | None = None, + offset: int | None = None, + limit: int | None = None, + description: str | None = None, + pattern: str | None = None, + include: str | None = None, + command: str | None = None, + arguments: str | None = None, + old_str: str | None = None, # Added because Claude often hallucinates this + new_str: str | None = None, # Added because Claude often hallucinates this + chat_id: str | None = None, # Added for chat identification + user_prompt: str | None = None, # Added for InitProject commit message + subject_line: str | None = None, # Added for InitProject commit message + reuse_head_chat_id: bool + | 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 + source_path: str | None = None, # Added for MV tool + target_path: str | None = None, # Added for MV tool + commit_hash: str | None = None, # Added for Git commit hash tracking +) -> str: + """Backward compatibility tool that forwards to the individual tools. + + This tool is provided for backward compatibility with existing code that uses + the codemcp tool via the old interface. It forwards to the appropriate tool + based on the subtool parameter. + + New code should use the individual tools directly instead of this one. + """ + logging.info(f"Forwarding codemcp({subtool}) to individual tool") + + try: + # Get all provided non-None parameters + provided_params = { + param: value + for param, value in { + "path": path, + "content": content, + "old_string": old_string, + "new_string": new_string, + "offset": offset, + "limit": limit, + "description": description, + "pattern": pattern, + "include": include, + "command": command, + "arguments": arguments, + # Include backward compatibility parameters + "old_str": old_str, + "new_str": new_str, + # Chat ID for session identification + "chat_id": chat_id, + # InitProject commit message parameters + "user_prompt": user_prompt, + "subject_line": subject_line, + # Whether to reuse the chat ID from the HEAD commit + "reuse_head_chat_id": reuse_head_chat_id, + # Think tool parameter + "thought": thought, + # Chmod tool parameter + "mode": mode, + # MV tool parameters + "source_path": source_path, + "target_path": target_path, + # Git commit hash tracking + "commit_hash": commit_hash, + }.items() + if value is not None + } + + # Direct calls to appropriate tools + if subtool == "ReadFile": + return await read_file(**provided_params) + elif subtool == "WriteFile": + return await write_file(**provided_params) + elif subtool == "EditFile": + return await edit_file(**provided_params) + elif subtool == "LS": + return await ls(**provided_params) + elif subtool == "InitProject": + # Handle parameter naming differences with adapter pattern in the central point + if "path" in provided_params and "directory" not in provided_params: + provided_params["directory"] = provided_params.pop("path") + return await init_project(**provided_params) + elif subtool == "RunCommand": + # Handle parameter naming differences with adapter pattern in the central point + if "path" in provided_params and "project_dir" not in provided_params: + provided_params["project_dir"] = provided_params.pop("path") + return await run_command(**provided_params) + elif subtool == "Grep": + return await grep(**provided_params) + elif subtool == "Glob": + return await glob(**provided_params) + elif subtool == "RM": + return await rm(**provided_params) + elif subtool == "MV": + return await mv(**provided_params) + elif subtool == "Think": + return await think(**provided_params) + elif subtool == "Chmod": + return await chmod(**provided_params) + else: + raise ValueError(f"Unknown subtool: {subtool}") + except Exception as e: + logging.error(f"Error in codemcp forwarding: {e}", exc_info=True) + raise + + # This should never be reached, but adding for type safety + return "Unknown subtool or operation" def get_files_respecting_gitignore(dir_path: Path, pattern: str = "**/*") -> List[Path]: diff --git a/codemcp/mcp.py b/codemcp/mcp.py new file mode 100644 index 0000000..e9ebcec --- /dev/null +++ b/codemcp/mcp.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 + +from mcp.server.fastmcp import FastMCP + +# Initialize FastMCP server +mcp = FastMCP("codemcp") + +__all__ = [ + "mcp", +] diff --git a/codemcp/tools/chmod.py b/codemcp/tools/chmod.py index 821827b..5a3a561 100644 --- a/codemcp/tools/chmod.py +++ b/codemcp/tools/chmod.py @@ -6,7 +6,7 @@ from ..common import normalize_file_path from ..git import commit_changes -from ..main import mcp +from ..mcp import mcp from ..shell import run_command from .commit_utils import append_commit_hash diff --git a/codemcp/tools/edit_file.py b/codemcp/tools/edit_file.py index bb4a7b2..66141fb 100644 --- a/codemcp/tools/edit_file.py +++ b/codemcp/tools/edit_file.py @@ -19,7 +19,7 @@ ) from ..git import commit_changes from ..line_endings import detect_line_endings -from ..main import mcp +from ..mcp import mcp from .commit_utils import append_commit_hash # Set up logger diff --git a/codemcp/tools/glob.py b/codemcp/tools/glob.py index 6192c6e..d8aed4f 100644 --- a/codemcp/tools/glob.py +++ b/codemcp/tools/glob.py @@ -7,7 +7,7 @@ from ..common import normalize_file_path from ..git import is_git_repository -from ..main import mcp +from ..mcp import mcp from .commit_utils import append_commit_hash __all__ = [ diff --git a/codemcp/tools/grep.py b/codemcp/tools/grep.py index 28f89f6..cd5e30a 100644 --- a/codemcp/tools/grep.py +++ b/codemcp/tools/grep.py @@ -7,7 +7,7 @@ from ..common import normalize_file_path from ..git import is_git_repository -from ..main import mcp +from ..mcp import mcp from ..shell import run_command from .commit_utils import append_commit_hash @@ -171,7 +171,7 @@ async def grep( Example: Grep "function.*hello" /path/to/repo # Find files containing functions with "hello" in their name - Grep "console\.log" /path/to/repo --include="*.js" # Find JS files with console.log statements + Grep "console\\.log" /path/to/repo --include="*.js" # Find JS files with console.log statements Args: pattern: The regular expression pattern to search for diff --git a/codemcp/tools/init_project.py b/codemcp/tools/init_project.py index f48e204..40dc1f0 100644 --- a/codemcp/tools/init_project.py +++ b/codemcp/tools/init_project.py @@ -10,7 +10,7 @@ from ..common import MAX_LINE_LENGTH, MAX_LINES_TO_READ, normalize_file_path from ..git import get_repository_root, is_git_repository -from ..main import mcp +from ..mcp import mcp __all__ = [ "init_project", diff --git a/codemcp/tools/ls.py b/codemcp/tools/ls.py index b39c62d..b290c46 100644 --- a/codemcp/tools/ls.py +++ b/codemcp/tools/ls.py @@ -7,7 +7,7 @@ from ..access import check_edit_permission from ..common import normalize_file_path from ..git import is_git_repository -from ..main import mcp +from ..mcp import mcp from .commit_utils import append_commit_hash __all__ = [ diff --git a/codemcp/tools/mv.py b/codemcp/tools/mv.py index 1440bc0..fef0297 100644 --- a/codemcp/tools/mv.py +++ b/codemcp/tools/mv.py @@ -6,7 +6,7 @@ from ..common import normalize_file_path from ..git import commit_changes, get_repository_root -from ..main import mcp +from ..mcp import mcp from ..shell import run_command from .commit_utils import append_commit_hash diff --git a/codemcp/tools/read_file.py b/codemcp/tools/read_file.py index 3802118..6f2f4a1 100644 --- a/codemcp/tools/read_file.py +++ b/codemcp/tools/read_file.py @@ -10,7 +10,7 @@ normalize_file_path, ) from ..git_query import find_git_root -from ..main import mcp +from ..mcp import mcp from ..rules import get_applicable_rules_content from .commit_utils import append_commit_hash diff --git a/codemcp/tools/rm.py b/codemcp/tools/rm.py index d9239f7..147d187 100644 --- a/codemcp/tools/rm.py +++ b/codemcp/tools/rm.py @@ -8,7 +8,7 @@ from ..access import check_edit_permission from ..common import normalize_file_path from ..git import commit_changes, get_repository_root, is_git_repository -from ..main import mcp +from ..mcp import mcp from ..shell import run_command from .commit_utils import append_commit_hash diff --git a/codemcp/tools/think.py b/codemcp/tools/think.py index b5f460b..87f8029 100644 --- a/codemcp/tools/think.py +++ b/codemcp/tools/think.py @@ -2,7 +2,7 @@ import logging -from ..main import mcp +from ..mcp import mcp __all__ = [ "think", diff --git a/codemcp/tools/write_file.py b/codemcp/tools/write_file.py index 36d8fc7..25e6725 100644 --- a/codemcp/tools/write_file.py +++ b/codemcp/tools/write_file.py @@ -13,7 +13,7 @@ ) from ..git import commit_changes from ..line_endings import detect_line_endings, detect_repo_line_endings -from ..main import mcp +from ..mcp import mcp from .commit_utils import append_commit_hash __all__ = [