diff --git a/codemcp/main.py b/codemcp/main.py index 54677917..ae6f649b 100644 --- a/codemcp/main.py +++ b/codemcp/main.py @@ -2,9 +2,7 @@ import logging import os -import sys from pathlib import Path -from typing import Optional import click from mcp.server.fastmcp import FastMCP diff --git a/codemcp/tools/edit_file.py b/codemcp/tools/edit_file.py index a4dac425..c61b6a40 100644 --- a/codemcp/tools/edit_file.py +++ b/codemcp/tools/edit_file.py @@ -24,8 +24,63 @@ __all__ = [ "edit_file_content", "find_similar_file", + "TOOL_NAME_FOR_PROMPT", + "DESCRIPTION", ] +TOOL_NAME_FOR_PROMPT = "EditFile" +DESCRIPTION = """ +This is a tool for editing files. For larger edits, use the Write tool to overwrite files. +Provide a short description of the change. + +Before using this tool: + +1. Use the View 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 /) + +If you want to create a new file, use: + - A new file path, including dir name if needed + - An empty old_string + - The new file's contents as new_string + +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. +""" + def find_similar_file(file_path: str) -> str | None: """Find a similar file with a different extension. diff --git a/codemcp/tools/glob.py b/codemcp/tools/glob.py index ce717489..ad8b3437 100644 --- a/codemcp/tools/glob.py +++ b/codemcp/tools/glob.py @@ -13,8 +13,18 @@ "glob_files", "glob", "render_result_for_assistant", + "TOOL_NAME_FOR_PROMPT", + "DESCRIPTION", ] +TOOL_NAME_FOR_PROMPT = "Glob" +DESCRIPTION = """ +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 +""" + # Define constants MAX_RESULTS = 100 diff --git a/codemcp/tools/init_project.py b/codemcp/tools/init_project.py index 94db8441..68ad9976 100644 --- a/codemcp/tools/init_project.py +++ b/codemcp/tools/init_project.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 import asyncio +import importlib import logging import os +import pkgutil import re +from typing import Dict import tomli @@ -12,6 +15,7 @@ __all__ = [ "init_project", + "collect_tool_descriptions", ] @@ -37,6 +41,46 @@ def _slugify(text: str) -> str: return text[:50] +def collect_tool_descriptions() -> Dict[str, str]: + """Collect tool name and description constants from all tools. + + This function scans all modules in the codemcp.tools package and looks for + TOOL_NAME_FOR_PROMPT and DESCRIPTION constants. + + Returns: + A dictionary mapping tool names to their descriptions + """ + tool_descriptions = {} + + # Import the tools package + import codemcp.tools as tools_package + + # Get the directory where the tools package is located + package_dir = os.path.dirname(tools_package.__file__) + + # Iterate through all modules in the tools package + for _, module_name, _ in pkgutil.iter_modules([package_dir]): + try: + # Skip __init__.py and this module to avoid circular imports + if module_name == "__init__" or module_name == "init_project": + continue + + # Import the module + module = importlib.import_module(f"codemcp.tools.{module_name}") + + # Check if the module has both constants + if hasattr(module, "TOOL_NAME_FOR_PROMPT") and hasattr( + module, "DESCRIPTION" + ): + tool_name = getattr(module, "TOOL_NAME_FOR_PROMPT") + description = getattr(module, "DESCRIPTION").strip() + tool_descriptions[tool_name] = description + except Exception as e: + logging.warning(f"Error importing module {module_name}: {e!s}") + + return tool_descriptions + + def _generate_command_docs(command_docs: dict) -> str: """Generate documentation for commands from the command_docs dictionary. @@ -246,7 +290,12 @@ async def init_project( # conveyed in chats. # TODO: This prompt is pretty long, maybe we want it shorter # NB: If you edit this, also edit codemcp/main.py - system_prompt = f"""\ + + # Collect tool descriptions from modules + tool_descriptions = collect_tool_descriptions() + + # Hard-coded section of the system prompt header + system_prompt_top = 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. # Tone and style @@ -490,6 +539,70 @@ async def init_project( When you use any tool, you MUST always include this chat ID as the chat_id parameter. """ + # Generate tool documentation section + tools_documentation = [] + + # Add tools with descriptions from the tool modules + for tool_name, description in sorted(tool_descriptions.items()): + # Handle special cases for tools that need custom formatting + if tool_name == "ReadFile": + # Format ReadFile with MAX_LINES_TO_READ and MAX_LINE_LENGTH + description = description.format( + MAX_LINES_TO_READ=MAX_LINES_TO_READ, MAX_LINE_LENGTH=MAX_LINE_LENGTH + ) + + # Add the tool documentation + tools_documentation.append( + f"## {tool_name} chat_id path arguments?\n\n{description}" + ) + + # Add RunCommand with special formatting for command_help and command_docs + run_command_doc = f""" +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)} +""" + tools_documentation.append( + f"## RunCommand chat_id path command arguments?\n\n{run_command_doc}" + ) + + # Summary section + summary_section = """ +## Summary + +Args: + subtool: The subtool to execute (ReadFile, WriteFile, EditFile, LS, InitProject, UserPrompt, RunCommand, RM, 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) + 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 section + chat_id_section = f""" +# Chat ID +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. +""" + + # Combine all sections to build the complete system prompt + system_prompt = system_prompt_top + system_prompt += "\n\n# codemcp tool\nThe codemcp tool supports a number of subtools which you should use to perform coding tasks.\n\n" + system_prompt += "\n\n".join(tools_documentation) + system_prompt += "\n" + summary_section + system_prompt += chat_id_section + # Combine system prompt, global prompt combined_prompt = system_prompt if project_prompt: diff --git a/codemcp/tools/ls.py b/codemcp/tools/ls.py index be0168a5..acffad2b 100644 --- a/codemcp/tools/ls.py +++ b/codemcp/tools/ls.py @@ -15,8 +15,15 @@ "create_file_tree", "print_tree", "MAX_FILES", + "TOOL_NAME_FOR_PROMPT", + "DESCRIPTION", ] +TOOL_NAME_FOR_PROMPT = "LS" +DESCRIPTION = """ +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. +""" + MAX_FILES = 1000 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" diff --git a/codemcp/tools/read_file.py b/codemcp/tools/read_file.py index 0f2a162b..f24d76fa 100644 --- a/codemcp/tools/read_file.py +++ b/codemcp/tools/read_file.py @@ -13,8 +13,15 @@ __all__ = [ "read_file_content", + "TOOL_NAME_FOR_PROMPT", + "DESCRIPTION", ] +TOOL_NAME_FOR_PROMPT = "ReadFile" +DESCRIPTION = """ +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. +""" + async def read_file_content( file_path: str, diff --git a/codemcp/tools/rm.py b/codemcp/tools/rm.py index 04cdb169..89833640 100644 --- a/codemcp/tools/rm.py +++ b/codemcp/tools/rm.py @@ -10,8 +10,25 @@ __all__ = [ "rm_file", + "TOOL_NAME_FOR_PROMPT", + "DESCRIPTION", ] +TOOL_NAME_FOR_PROMPT = "RM" +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 +""" + async def rm_file( path: str, diff --git a/codemcp/tools/run_command.py b/codemcp/tools/run_command.py index 4a1961d0..1870e616 100644 --- a/codemcp/tools/run_command.py +++ b/codemcp/tools/run_command.py @@ -7,8 +7,24 @@ __all__ = [ "run_command", + "TOOL_NAME_FOR_PROMPT", + "DESCRIPTION", ] +TOOL_NAME_FOR_PROMPT = "RunCommand" +DESCRIPTION = """ +Runs a command. This does NOT support arbitrary code execution, ONLY call +with this set of valid commands: format, lint, ghstack, 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. +""" + async def run_command( project_dir: str, diff --git a/codemcp/tools/think.py b/codemcp/tools/think.py index 915c602f..0a99a7e5 100644 --- a/codemcp/tools/think.py +++ b/codemcp/tools/think.py @@ -4,8 +4,15 @@ __all__ = [ "think", + "TOOL_NAME_FOR_PROMPT", + "DESCRIPTION", ] +TOOL_NAME_FOR_PROMPT = "Think" +DESCRIPTION = """ +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. +""" + async def think(thought: str, chat_id: str | None = None) -> str: """Use this tool to think about something without obtaining new information or changing the database. diff --git a/codemcp/tools/user_prompt.py b/codemcp/tools/user_prompt.py index 0f19a951..9abf1847 100644 --- a/codemcp/tools/user_prompt.py +++ b/codemcp/tools/user_prompt.py @@ -8,8 +8,17 @@ __all__ = [ "user_prompt", + "TOOL_NAME_FOR_PROMPT", + "DESCRIPTION", ] +TOOL_NAME_FOR_PROMPT = "UserPrompt" +DESCRIPTION = """ +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. +""" + async def user_prompt(user_text: str, chat_id: str | None = None) -> str: """Store the user's verbatim prompt text for later use. diff --git a/codemcp/tools/write_file.py b/codemcp/tools/write_file.py index ea1e679f..57bde9d6 100644 --- a/codemcp/tools/write_file.py +++ b/codemcp/tools/write_file.py @@ -12,8 +12,23 @@ __all__ = [ "write_file_content", + "TOOL_NAME_FOR_PROMPT", + "DESCRIPTION", ] +TOOL_NAME_FOR_PROMPT = "WriteFile" +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 +""" + async def write_file_content( file_path: str, content: str, description: str = "", chat_id: str = None