Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions codemcp/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions codemcp/tools/edit_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions codemcp/tools/glob.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
115 changes: 114 additions & 1 deletion codemcp/tools/init_project.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -12,6 +15,7 @@

__all__ = [
"init_project",
"collect_tool_descriptions",
]


Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions codemcp/tools/ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
7 changes: 7 additions & 0 deletions codemcp/tools/read_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions codemcp/tools/rm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions codemcp/tools/run_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions codemcp/tools/think.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions codemcp/tools/user_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions codemcp/tools/write_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading