From 5d78e3f24e07512bd9712685604dc4b0faf53fc4 Mon Sep 17 00:00:00 2001 From: "Edward Z. Yang" Date: Sat, 3 May 2025 21:00:15 -0400 Subject: [PATCH] Update [ghstack-poisoned] --- codemcp/main.py | 13 +- codemcp/tools/chmod.py | 28 +++- codemcp/tools/edit_file.py | 60 ++++++++ codemcp/tools/git_blame.py | 27 +++- codemcp/tools/git_diff.py | 28 +++- codemcp/tools/git_log.py | 25 +++- codemcp/tools/git_show.py | 29 +++- codemcp/tools/glob.py | 19 +++ codemcp/tools/grep.py | 24 +++ codemcp/tools/init_project.py | 271 +++++----------------------------- codemcp/tools/ls.py | 14 ++ codemcp/tools/mv.py | 26 ++++ codemcp/tools/read_file.py | 19 +++ codemcp/tools/rm.py | 22 +++ codemcp/tools/run_command.py | 24 +++ codemcp/tools/think.py | 15 ++ codemcp/tools/user_prompt.py | 16 ++ codemcp/tools/write_file.py | 23 +++ 18 files changed, 439 insertions(+), 244 deletions(-) diff --git a/codemcp/main.py b/codemcp/main.py index a249842d..2610d70c 100644 --- a/codemcp/main.py +++ b/codemcp/main.py @@ -10,7 +10,7 @@ import pathspec import uvicorn from fastapi.middleware.cors import CORSMiddleware -from mcp.server.fastmcp import FastMCP +from mcp.server.fastmcp import Context, FastMCP from starlette.applications import Starlette from starlette.routing import Mount @@ -34,6 +34,13 @@ mcp = FastMCP("codemcp") +# Helper function to get a chat_id from a Context +def get_chat_id_from_context(ctx: Context) -> str: + # Generate a chat_id from the context + ctx_id = getattr(ctx, "id", None) + return f"{ctx_id}" if ctx_id else "default" + + # 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. @@ -62,8 +69,8 @@ async def append_commit_hash(result: str, path: str | None) -> Tuple[str, str | return result, current_hash -# NB: If you edit this, also edit codemcp/tools/init_project.py -@mcp.tool() +# This function is kept for backward compatibility but is no longer directly exposed as a tool +# Each subtool is individually exposed as a top-level MCP tool async def codemcp( subtool: str, *, diff --git a/codemcp/tools/chmod.py b/codemcp/tools/chmod.py index 9b78e0f9..7e58a26b 100644 --- a/codemcp/tools/chmod.py +++ b/codemcp/tools/chmod.py @@ -4,8 +4,11 @@ import stat from typing import Any, Literal +from mcp.server.fastmcp import Context + from ..common import normalize_file_path from ..git import commit_changes +from ..main import get_chat_id_from_context, mcp from ..shell import run_command __all__ = [ @@ -13,12 +16,13 @@ "render_result_for_assistant", "TOOL_NAME_FOR_PROMPT", "DESCRIPTION", + "chmod_tool", ] 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: @@ -129,3 +133,23 @@ def render_result_for_assistant(output: dict[str, Any]) -> str: A formatted string representation of the results """ return output.get("output", "") + + +@mcp.tool() +async def chmod_tool(ctx: Context, path: str, mode: Literal["a+x", "a-x"]) -> str: + """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 absolute path to the file to modify + mode: The chmod mode to apply, only "a+x" and "a-x" are supported + + 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 + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + result = await chmod(path, mode, chat_id) + return result.get("resultForAssistant", "Chmod operation completed") diff --git a/codemcp/tools/edit_file.py b/codemcp/tools/edit_file.py index 081947a6..fa8c8a3a 100644 --- a/codemcp/tools/edit_file.py +++ b/codemcp/tools/edit_file.py @@ -9,6 +9,8 @@ from difflib import SequenceMatcher from typing import Any, Dict, List, Tuple +from mcp.server.fastmcp import Context + from ..code_command import run_formatter_without_commit from ..common import get_edit_snippet from ..file_utils import ( @@ -19,6 +21,7 @@ ) from ..git import commit_changes from ..line_endings import detect_line_endings +from ..main import get_chat_id_from_context, mcp # Set up logger logger = logging.getLogger(__name__) @@ -26,6 +29,7 @@ __all__ = [ "edit_file_content", "find_similar_file", + "edit_file", ] @@ -805,3 +809,59 @@ async def edit_file_content( git_message = f"\n\nFailed to commit changes to git: {message}" return f"Successfully edited {full_file_path}\n\nHere's a snippet of the edited file:\n{snippet}{format_message}{git_message}" + + +@mcp.tool() +async def edit_file( + ctx: Context, file_path: str, old_string: str, new_string: str, description: str +) -> str: + """This is a tool for editing files. For larger edits, use the WriteFile tool to overwrite files. + 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. 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. + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + return await edit_file_content( + file_path, old_string, new_string, None, description, chat_id + ) diff --git a/codemcp/tools/git_blame.py b/codemcp/tools/git_blame.py index 07ad1d4e..ad86826d 100644 --- a/codemcp/tools/git_blame.py +++ b/codemcp/tools/git_blame.py @@ -2,10 +2,13 @@ import logging import shlex -from typing import Any +from typing import Any, Optional + +from mcp.server.fastmcp import Context from ..common import normalize_file_path from ..git import is_git_repository +from ..main import get_chat_id_from_context, mcp from ..shell import run_command __all__ = [ @@ -13,6 +16,7 @@ "render_result_for_assistant", "TOOL_NAME_FOR_PROMPT", "DESCRIPTION", + "git_blame_tool", ] TOOL_NAME_FOR_PROMPT = "GitBlame" @@ -96,3 +100,24 @@ def render_result_for_assistant(output: dict[str, Any]) -> str: A formatted string representation of the results """ return output.get("output", "") + + +@mcp.tool() +async def git_blame_tool( + ctx: Context, path: str, arguments: Optional[str] = None +) -> str: + """Shows what revision and author last modified each line of a file using git blame. + This tool is read-only and safe to use with any arguments. + The arguments parameter should be a string and will be interpreted as space-separated + arguments using shell-style tokenization (spaces separate arguments, quotes can be used + for arguments containing spaces, etc.). + + Example: + git blame path/to/file # Show blame information for a file + git blame -L 10,20 path/to/file # Show blame information for lines 10-20 + git blame -w path/to/file # Ignore whitespace changes + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + result = await git_blame(arguments, path, chat_id) + return result.get("resultForAssistant", "") diff --git a/codemcp/tools/git_diff.py b/codemcp/tools/git_diff.py index b91a45db..6a30b653 100644 --- a/codemcp/tools/git_diff.py +++ b/codemcp/tools/git_diff.py @@ -2,10 +2,13 @@ import logging import shlex -from typing import Any +from typing import Any, Optional + +from mcp.server.fastmcp import Context from ..common import normalize_file_path from ..git import is_git_repository +from ..main import get_chat_id_from_context, mcp from ..shell import run_command __all__ = [ @@ -13,6 +16,7 @@ "render_result_for_assistant", "TOOL_NAME_FOR_PROMPT", "DESCRIPTION", + "git_diff_tool", ] TOOL_NAME_FOR_PROMPT = "GitDiff" @@ -97,3 +101,25 @@ def render_result_for_assistant(output: dict[str, Any]) -> str: A formatted string representation of the results """ return output.get("output", "") + + +@mcp.tool() +async def git_diff_tool( + ctx: Context, path: str, arguments: Optional[str] = None +) -> str: + """Shows differences between commits, commit and working tree, etc. using git diff. + This tool is read-only and safe to use with any arguments. + The arguments parameter should be a string and will be interpreted as space-separated + arguments using shell-style tokenization (spaces separate arguments, quotes can be used + for arguments containing spaces, etc.). + + Example: + git diff # Show changes between working directory and index + git diff HEAD~1 # Show changes between current commit and previous commit + git diff branch1 branch2 # Show differences between two branches + git diff --stat # Show summary of changes instead of full diff + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + result = await git_diff(arguments, path, chat_id) + return result.get("resultForAssistant", "") diff --git a/codemcp/tools/git_log.py b/codemcp/tools/git_log.py index a1370913..d9693960 100644 --- a/codemcp/tools/git_log.py +++ b/codemcp/tools/git_log.py @@ -2,10 +2,13 @@ import logging import shlex -from typing import Any +from typing import Any, Optional + +from mcp.server.fastmcp import Context from ..common import normalize_file_path from ..git import is_git_repository +from ..main import get_chat_id_from_context, mcp from ..shell import run_command __all__ = [ @@ -13,6 +16,7 @@ "render_result_for_assistant", "TOOL_NAME_FOR_PROMPT", "DESCRIPTION", + "git_log_tool", ] TOOL_NAME_FOR_PROMPT = "GitLog" @@ -96,3 +100,22 @@ def render_result_for_assistant(output: dict[str, Any]) -> str: A formatted string representation of the results """ return output.get("output", "") + + +@mcp.tool() +async def git_log_tool(ctx: Context, path: str, arguments: Optional[str] = None) -> str: + """Shows commit logs using git log. + This tool is read-only and safe to use with any arguments. + The arguments parameter should be a string and will be interpreted as space-separated + arguments using shell-style tokenization (spaces separate arguments, quotes can be used + for arguments containing spaces, etc.). + + Example: + git log --oneline -n 5 # Show the last 5 commits in oneline format + git log --author="John Doe" --since="2023-01-01" # Show commits by an author since a date + git log -- path/to/file # Show commit history for a specific file + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + result = await git_log(arguments, path, chat_id) + return result.get("resultForAssistant", "") diff --git a/codemcp/tools/git_show.py b/codemcp/tools/git_show.py index 2d68592a..a7b65372 100644 --- a/codemcp/tools/git_show.py +++ b/codemcp/tools/git_show.py @@ -2,10 +2,13 @@ import logging import shlex -from typing import Any +from typing import Any, Optional + +from mcp.server.fastmcp import Context from ..common import normalize_file_path from ..git import is_git_repository +from ..main import get_chat_id_from_context, mcp from ..shell import run_command __all__ = [ @@ -13,6 +16,7 @@ "render_result_for_assistant", "TOOL_NAME_FOR_PROMPT", "DESCRIPTION", + "git_show_tool", ] TOOL_NAME_FOR_PROMPT = "GitShow" @@ -98,3 +102,26 @@ def render_result_for_assistant(output: dict[str, Any]) -> str: A formatted string representation of the results """ return output.get("output", "") + + +@mcp.tool() +async def git_show_tool( + ctx: Context, path: str, arguments: Optional[str] = None +) -> str: + """Shows various types of objects (commits, tags, trees, blobs) using git show. + This tool is read-only and safe to use with any arguments. + The arguments parameter should be a string and will be interpreted as space-separated + arguments using shell-style tokenization (spaces separate arguments, quotes can be used + for arguments containing spaces, etc.). + + Example: + git show # Show the most recent commit + git show a1b2c3d # Show a specific commit by hash + git show HEAD~3 # Show the commit 3 before HEAD + git show v1.0 # Show a tag + git show HEAD:path/to/file # Show a file from a specific commit + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + result = await git_show(arguments, path, chat_id) + return result.get("resultForAssistant", "") diff --git a/codemcp/tools/glob.py b/codemcp/tools/glob.py index c98114e8..eeceadc4 100644 --- a/codemcp/tools/glob.py +++ b/codemcp/tools/glob.py @@ -6,12 +6,16 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple +from mcp.server.fastmcp import Context + from ..common import normalize_file_path +from ..main import get_chat_id_from_context, mcp __all__ = [ "glob_files", "glob", "render_result_for_assistant", + "glob_tool", ] # Define constants @@ -188,3 +192,18 @@ async def glob_files( output["resultForAssistant"] = render_result_for_assistant(output) return output + + +@mcp.tool() +async def glob_tool(ctx: Context, pattern: str, path: str) -> str: + """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 + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + result = await glob_files(pattern, path, MAX_RESULTS, 0, chat_id) + return result.get( + "resultForAssistant", f"Found {result.get('numFiles', 0)} file(s)" + ) diff --git a/codemcp/tools/grep.py b/codemcp/tools/grep.py index fde90dfb..7f1819e6 100644 --- a/codemcp/tools/grep.py +++ b/codemcp/tools/grep.py @@ -5,8 +5,11 @@ import subprocess from typing import Any, Dict, List, Optional, Tuple +from mcp.server.fastmcp import Context + from ..common import normalize_file_path from ..git import is_git_repository +from ..main import get_chat_id_from_context, mcp from ..shell import run_command __all__ = [ @@ -15,6 +18,7 @@ "render_result_for_assistant", "TOOL_NAME_FOR_PROMPT", "DESCRIPTION", + "grep", ] # Define constants @@ -217,3 +221,23 @@ async def grep_files( output["resultForAssistant"] = formatted_result return output + + +@mcp.tool() +async def grep( + ctx: Context, pattern: str, path: str, include: str | None = None +) -> str: + """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 + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + result = await grep_files(pattern, path, include, chat_id) + return result.get( + "resultForAssistant", f"Found {result.get('numFiles', 0)} file(s)" + ) diff --git a/codemcp/tools/init_project.py b/codemcp/tools/init_project.py index 84f0b34e..1648eb54 100644 --- a/codemcp/tools/init_project.py +++ b/codemcp/tools/init_project.py @@ -7,12 +7,15 @@ from typing import Any, Dict, List, Optional import tomli +from mcp.server.fastmcp import Context -from ..common import MAX_LINE_LENGTH, MAX_LINES_TO_READ, normalize_file_path +from ..common import normalize_file_path from ..git import get_repository_root, is_git_repository +from ..main import mcp __all__ = [ "init_project", + "initialize_project", ] @@ -206,7 +209,6 @@ async def init_project( ) project_prompt = "" - command_help = "" command_docs: Dict[str, str] = {} rules_config: Dict[str, Any] = {} @@ -226,7 +228,7 @@ async def init_project( # Extract commands and their documentation command_list = rules_config.get("commands", {}) - command_help = ", ".join(command_list.keys()) + ", ".join(command_list.keys()) # Process command documentation for cmd_name, cmd_config in command_list.items(): @@ -240,13 +242,8 @@ async def init_project( ) raise ValueError(f"Error reading codemcp.toml file: {e!s}") - # Default system prompt, cribbed from claude code - # TODO: Figure out if we want Sonnet to make determinations about what - # goes in the global prompt. The current ARCHITECTURE.md rule is - # mostly to make sure we don't lose important information that was - # conveyed in chats. - # TODO: This prompt is pretty long, maybe we want it shorter - # NB: If you edit this, also edit codemcp/main.py + # Default system prompt - now with most of the tool docs moved to the function docstrings + # Keep the non-tool prompts here system_prompt = f"""\ You are an AI assistant that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user. @@ -274,237 +271,13 @@ async def init_project( # Tool usage policy - If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in the same function_calls block. -# codemcp tool -The codemcp tool supports a number of subtools which you should use to perform coding tasks. - -## GitLog chat_id path arguments? - -Shows commit logs using git log. -This tool is read-only and safe to use with any arguments. -The arguments parameter should be a string and will be interpreted as space-separated -arguments using shell-style tokenization (spaces separate arguments, quotes can be used -for arguments containing spaces, etc.). - -Example: - git log --oneline -n 5 # Show the last 5 commits in oneline format - git log --author="John Doe" --since="2023-01-01" # Show commits by an author since a date - git log -- path/to/file # Show commit history for a specific file - -## GitDiff chat_id path arguments? - -Shows differences between commits, commit and working tree, etc. using git diff. -This tool is read-only and safe to use with any arguments. -The arguments parameter should be a string and will be interpreted as space-separated -arguments using shell-style tokenization (spaces separate arguments, quotes can be used -for arguments containing spaces, etc.). - -Example: - git diff # Show changes between working directory and index - git diff HEAD~1 # Show changes between current commit and previous commit - git diff branch1 branch2 # Show differences between two branches - git diff --stat # Show summary of changes instead of full diff - -## GitShow chat_id path arguments? - -Shows various types of objects (commits, tags, trees, blobs) using git show. -This tool is read-only and safe to use with any arguments. -The arguments parameter should be a string and will be interpreted as space-separated -arguments using shell-style tokenization (spaces separate arguments, quotes can be used -for arguments containing spaces, etc.). - -Example: - git show # Show the most recent commit - git show a1b2c3d # Show a specific commit by hash - git show HEAD~3 # Show the commit 3 before HEAD - git show v1.0 # Show a tag - git show HEAD:path/to/file # Show a file from a specific commit - -## GitBlame chat_id path arguments? - -Shows what revision and author last modified each line of a file using git blame. -This tool is read-only and safe to use with any arguments. -The arguments parameter should be a string and will be interpreted as space-separated -arguments using shell-style tokenization (spaces separate arguments, quotes can be used -for arguments containing spaces, etc.). - -Example: - git blame path/to/file # Show blame information for a file - git blame -L 10,20 path/to/file # Show blame information for lines 10-20 - git blame -w path/to/file # Ignore whitespace changes - -## ReadFile chat_id path offset? 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 {MAX_LINES_TO_READ} 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 {MAX_LINE_LENGTH} characters will be truncated. For image files, the tool will display the image for you. - -## WriteFile chat_id path content description - -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 - -## EditFile chat_id path old_string new_string description - -This is a tool for editing files. For larger edits, use the WriteFile tool to overwrite files. -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. 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. - -## UserPrompt chat_id user_prompt - -Records the user's verbatim prompt text for each interaction after the initial one. -You should call this tool with the user's exact message at the beginning of each response. -This tool must be called in every response except for the first one where InitProject was used. Do NOT include documents or other attachments, only the text prompt. - -## Think chat_id thought - -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. - -## LS chat_id path - -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. - -## Glob chat_id pattern path - -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 - -## Grep chat_id pattern path include? - -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 - -## RunCommand chat_id path command arguments? - -Runs a command. This does NOT support arbitrary code execution, ONLY call -with this set of valid commands: {command_help} -The arguments parameter should be a string and will be interpreted as space-separated -arguments using shell-style tokenization (spaces separate arguments, quotes can be used -for arguments containing spaces, etc.). -{_generate_command_docs(command_docs)} - -## RM chat_id path description - -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 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 - -## MV chat_id source_path target_path description - -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 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 to identify the chat session - -## 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 -are the only bits that git knows how to track. - -Args: - 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 to identify the chat session - -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 - -## Summary - -Args: - subtool: The subtool to execute (ReadFile, WriteFile, EditFile, LS, InitProject, UserPrompt, RunCommand, RM, MV, Think, Chmod) - path: The path to the file or directory to operate on - content: Content for WriteFile subtool (any type will be serialized to string if needed) - old_string: String to replace for EditFile subtool - new_string: Replacement string for EditFile subtool - offset: Line offset for ReadFile subtool - limit: Line limit for ReadFile subtool - description: Short description of the change (for WriteFile/EditFile/RM/MV) - source_path: The path to the source file for MV subtool - target_path: The destination path for MV subtool - arguments: A string containing space-separated arguments for RunCommand subtool - user_prompt: The user's verbatim text (for UserPrompt subtool) - thought: The thought content (for Think subtool) - 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 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. +{_generate_command_docs(command_docs)} """ # Combine system prompt, global prompt @@ -518,3 +291,31 @@ async def init_project( f"Exception suppressed during project initialization: {e!s}", exc_info=True ) return f"Error initializing project: {e!s}" + + +@mcp.tool() +async def initialize_project( + ctx: Context, + file_path: str, + user_prompt: str, + subject_line: str, + reuse_head_chat_id: bool = False, +) -> str: + """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. + + In each subsequent request NOT including the initial request to initialize codemcp, you must call + the UserPrompt tool with the user's verbatim request text. + + Arguments: + file_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 + reuse_head_chat_id: If True, reuse the chat ID from the HEAD commit instead of generating a new one + """ + # For init_project, we don't use the context's chat_id, as it generates its own + return await init_project(file_path, user_prompt, subject_line, reuse_head_chat_id) diff --git a/codemcp/tools/ls.py b/codemcp/tools/ls.py index 42603661..b7e44327 100644 --- a/codemcp/tools/ls.py +++ b/codemcp/tools/ls.py @@ -4,9 +4,12 @@ import os from typing import List, Optional +from mcp.server.fastmcp import Context + from ..access import check_edit_permission from ..common import normalize_file_path from ..git import is_git_repository +from ..main import get_chat_id_from_context, mcp __all__ = [ "ls_directory", @@ -16,6 +19,7 @@ "create_file_tree", "print_tree", "MAX_FILES", + "ls", ] MAX_FILES = 1000 @@ -231,3 +235,13 @@ def print_tree( result += print_tree(node.children, level + 1, f"{prefix} ", cwd) return result + + +@mcp.tool() +async def ls(ctx: Context, file_path: str) -> str: + """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. + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + return await ls_directory(file_path, chat_id) diff --git a/codemcp/tools/mv.py b/codemcp/tools/mv.py index bcffd750..cc08518f 100644 --- a/codemcp/tools/mv.py +++ b/codemcp/tools/mv.py @@ -4,12 +4,16 @@ import os import pathlib +from mcp.server.fastmcp import Context + from ..common import normalize_file_path from ..git import commit_changes, get_repository_root +from ..main import get_chat_id_from_context, mcp from ..shell import run_command __all__ = [ "mv_file", + "mv_tool", ] @@ -133,3 +137,25 @@ async def mv_file( return f"Successfully moved file from {source_rel_path} to {target_rel_path}." else: return f"File was moved from {source_rel_path} to {target_rel_path} but failed to commit: {commit_message}" + + +@mcp.tool() +async def mv_tool( + ctx: Context, source_path: str, target_path: str, description: str +) -> str: + """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 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 + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + return await mv_file(source_path, target_path, description, chat_id) diff --git a/codemcp/tools/read_file.py b/codemcp/tools/read_file.py index 2f392b90..aea4018d 100644 --- a/codemcp/tools/read_file.py +++ b/codemcp/tools/read_file.py @@ -3,6 +3,8 @@ import os from typing import List +from mcp.server.fastmcp import Context + from ..common import ( MAX_LINE_LENGTH, MAX_LINES_TO_READ, @@ -10,10 +12,12 @@ normalize_file_path, ) from ..git_query import find_git_root +from ..main import get_chat_id_from_context, mcp from ..rules import get_applicable_rules_content __all__ = [ "read_file_content", + "read_file", ] @@ -106,3 +110,18 @@ async def read_file_content( content += get_applicable_rules_content(repo_root, full_file_path) return content + + +@mcp.tool() +async def read_file( + ctx: Context, file_path: str, offset: int | None = None, limit: int | None = None +) -> str: + """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. + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + return await read_file_content(file_path, offset, limit, chat_id) diff --git a/codemcp/tools/rm.py b/codemcp/tools/rm.py index 0c04ff4c..b0b45d17 100644 --- a/codemcp/tools/rm.py +++ b/codemcp/tools/rm.py @@ -4,12 +4,16 @@ import os import pathlib +from mcp.server.fastmcp import Context + from ..common import normalize_file_path from ..git import commit_changes, get_repository_root +from ..main import get_chat_id_from_context, mcp from ..shell import run_command __all__ = [ "rm_file", + "rm_tool", ] @@ -94,3 +98,21 @@ async def rm_file( return f"Successfully removed file {rel_path}." else: return f"File {rel_path} was removed but failed to commit: {commit_message}" + + +@mcp.tool() +async def rm_tool(ctx: Context, path: str, description: str) -> str: + """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 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 + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + return await rm_file(path, description, chat_id) diff --git a/codemcp/tools/run_command.py b/codemcp/tools/run_command.py index 23b52b86..7660143b 100644 --- a/codemcp/tools/run_command.py +++ b/codemcp/tools/run_command.py @@ -3,10 +3,14 @@ import shlex from typing import Optional +from mcp.server.fastmcp import Context + from ..code_command import get_command_from_config, run_code_command +from ..main import get_chat_id_from_context, mcp __all__ = [ "run_command", + "run_command_tool", ] @@ -44,3 +48,23 @@ async def run_command( return await run_code_command( project_dir, command, actual_command, f"Auto-commit {command} changes", chat_id ) + + +@mcp.tool() +async def run_command_tool( + ctx: Context, path: str, command: str, arguments: Optional[str] = None +) -> str: + """Runs a command. This does NOT support arbitrary code execution, ONLY call + with this set of valid commands: format, lint, ghstack, typecheck, test, accept + The arguments parameter should be a string and will be interpreted as space-separated + arguments using shell-style tokenization (spaces separate arguments, quotes can be used + for arguments containing spaces, etc.). + + Command documentation: + - test: Accepts a pytest-style test selector as an argument to run a specific test. + - accept: Updates expecttest failing tests with their new values, akin to running with EXPECTTEST_ACCEPT=1. + Accepts a pytest-style test selector as an argument to run a specific test. + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + return await run_command(path, command, arguments, chat_id) diff --git a/codemcp/tools/think.py b/codemcp/tools/think.py index 915c602f..954f0164 100644 --- a/codemcp/tools/think.py +++ b/codemcp/tools/think.py @@ -2,8 +2,13 @@ import logging +from mcp.server.fastmcp import Context + +from ..main import get_chat_id_from_context, mcp + __all__ = [ "think", + "think_tool", ] @@ -22,3 +27,13 @@ async def think(thought: str, chat_id: str | None = None) -> str: # Return a simple confirmation message return f"Thought logged: {thought}" + + +@mcp.tool() +async def think_tool(ctx: Context, thought: str) -> str: + """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. + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + return await think(thought, chat_id) diff --git a/codemcp/tools/user_prompt.py b/codemcp/tools/user_prompt.py index 0f19a951..7853b192 100644 --- a/codemcp/tools/user_prompt.py +++ b/codemcp/tools/user_prompt.py @@ -3,11 +3,15 @@ import logging import os +from mcp.server.fastmcp import Context + from ..git_query import find_git_root +from ..main import get_chat_id_from_context, mcp from ..rules import get_applicable_rules_content __all__ = [ "user_prompt", + "record_user_prompt", ] @@ -37,3 +41,15 @@ async def user_prompt(user_text: str, chat_id: str | None = None) -> str: result += get_applicable_rules_content(repo_root) return result + + +@mcp.tool() +async def record_user_prompt(ctx: Context, user_prompt: str) -> str: + """Records the user's verbatim prompt text for each interaction after the initial one. + You should call this tool with the user's exact message at the beginning of each response. + This tool must be called in every response except for the first one where InitProject was used. + Do NOT include documents or other attachments, only the text prompt. + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + return await user_prompt(user_prompt, chat_id) diff --git a/codemcp/tools/write_file.py b/codemcp/tools/write_file.py index 3efa8f45..c145f4b9 100644 --- a/codemcp/tools/write_file.py +++ b/codemcp/tools/write_file.py @@ -3,6 +3,8 @@ import logging import os +from mcp.server.fastmcp import Context + from ..code_command import run_formatter_without_commit from ..file_utils import ( check_file_path_and_permissions, @@ -11,9 +13,11 @@ ) from ..git import commit_changes from ..line_endings import detect_line_endings, detect_repo_line_endings +from ..main import get_chat_id_from_context, mcp __all__ = [ "write_file_content", + "write_file", ] @@ -84,3 +88,22 @@ async def write_file_content( git_message = f"\nFailed to commit changes to git: {message}" return f"Successfully wrote to {file_path}{format_message}{git_message}" + + +@mcp.tool() +async def write_file( + ctx: Context, file_path: str, content: str, description: str +) -> str: + """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 + """ + # Get chat ID from context + chat_id = get_chat_id_from_context(ctx) + return await write_file_content(file_path, content, description, chat_id)