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
13 changes: 10 additions & 3 deletions codemcp/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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,
*,
Expand Down
28 changes: 26 additions & 2 deletions codemcp/tools/chmod.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@
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__ = [
"chmod",
"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:
Expand Down Expand Up @@ -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")
60 changes: 60 additions & 0 deletions codemcp/tools/edit_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -19,13 +21,15 @@
)
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__)

__all__ = [
"edit_file_content",
"find_similar_file",
"edit_file",
]


Expand Down Expand Up @@ -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
)
27 changes: 26 additions & 1 deletion codemcp/tools/git_blame.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

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__ = [
"git_blame",
"render_result_for_assistant",
"TOOL_NAME_FOR_PROMPT",
"DESCRIPTION",
"git_blame_tool",
]

TOOL_NAME_FOR_PROMPT = "GitBlame"
Expand Down Expand Up @@ -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", "")
28 changes: 27 additions & 1 deletion codemcp/tools/git_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

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__ = [
"git_diff",
"render_result_for_assistant",
"TOOL_NAME_FOR_PROMPT",
"DESCRIPTION",
"git_diff_tool",
]

TOOL_NAME_FOR_PROMPT = "GitDiff"
Expand Down Expand Up @@ -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", "")
25 changes: 24 additions & 1 deletion codemcp/tools/git_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

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__ = [
"git_log",
"render_result_for_assistant",
"TOOL_NAME_FOR_PROMPT",
"DESCRIPTION",
"git_log_tool",
]

TOOL_NAME_FOR_PROMPT = "GitLog"
Expand Down Expand Up @@ -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", "")
29 changes: 28 additions & 1 deletion codemcp/tools/git_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

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__ = [
"git_show",
"render_result_for_assistant",
"TOOL_NAME_FOR_PROMPT",
"DESCRIPTION",
"git_show_tool",
]

TOOL_NAME_FOR_PROMPT = "GitShow"
Expand Down Expand Up @@ -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", "")
19 changes: 19 additions & 0 deletions codemcp/tools/glob.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)"
)
Loading
Loading