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
57 changes: 48 additions & 9 deletions src/code_review_mcp/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ async def add_inline_comment(
line: int,
line_type: str,
comment: str,
start_line: int | None = None,
start_line_type: str | None = None,
) -> dict[str, Any]:
"""Add inline comment to specific line."""
"""Add inline comment to specific line or range of lines."""
pass

@abstractmethod
Expand Down Expand Up @@ -162,8 +164,14 @@ async def get_pr_changes(
"total_files": len(filtered_changes),
}

def _find_line_code(self, diff: str, target_line: int, line_type: str, head_sha: str) -> str:
"""Find line_code from diff for GitLab API."""
def _find_line_info(
self, diff: str, target_line: int, line_type: str, head_sha: str
) -> tuple[str, int | None, int | None]:
"""Find line_code and line numbers from diff for GitLab API.

Returns (line_code, old_line_num, new_line_num).
For 'old' lines: new_line_num is None; for 'new' lines: old_line_num is the context value.
"""
lines = diff.split("\n")
old_line = 0
new_line = 0
Expand All @@ -177,16 +185,16 @@ def _find_line_code(self, diff: str, target_line: int, line_type: str, head_sha:
elif line.startswith("-"):
old_line += 1
if line_type == "old" and old_line == target_line:
return f"{head_sha}_{old_line}_"
return f"{head_sha}_{old_line}_", old_line, None
elif line.startswith("+"):
new_line += 1
if line_type == "new" and new_line == target_line:
return f"{head_sha}_{old_line}_{new_line}"
return f"{head_sha}_{old_line}_{new_line}", None, new_line
else:
old_line += 1
new_line += 1

return ""
return "", None, None

async def add_inline_comment(
self,
Expand All @@ -196,6 +204,8 @@ async def add_inline_comment(
line: int,
line_type: str,
comment: str,
start_line: int | None = None,
start_line_type: str | None = None,
) -> dict[str, Any]:
"""Add inline comment."""
project_id = repo.replace("/", "%2F")
Expand All @@ -217,8 +227,9 @@ async def add_inline_comment(
if not target_diff:
return {"success": False, "error": f"File not found: {file_path}"}

line_code = self._find_line_code(
target_diff, line, line_type, mr_info.get("diff_refs", {}).get("head_sha", "")
head_sha = mr_info.get("diff_refs", {}).get("head_sha", "")
line_code, end_old_line, end_new_line = self._find_line_info(
target_diff, line, line_type, head_sha
)
if not line_code:
return {"success": False, "error": f"Cannot locate line {line}"}
Expand All @@ -239,6 +250,28 @@ async def add_inline_comment(
else:
position["new_line"] = line

if start_line is not None:
effective_start_type = start_line_type or line_type
start_code, start_old_line, start_new_line = self._find_line_info(
target_diff, start_line, effective_start_type, head_sha
)
if not start_code:
return {"success": False, "error": f"Cannot locate start_line {start_line}"}
position["line_range"] = {
"start": {
"line_code": start_code,
"type": effective_start_type,
"old_line": start_old_line,
"new_line": start_new_line,
},
"end": {
"line_code": line_code,
"type": line_type,
"old_line": end_old_line,
"new_line": end_new_line,
},
}

data = {"body": comment, "position": position}
result = await self._call_api(
project_id, f"merge_requests/{pr_id}/discussions", method="POST", data=data
Expand Down Expand Up @@ -394,6 +427,8 @@ async def add_inline_comment(
line: int,
line_type: str,
comment: str,
start_line: int | None = None,
start_line_type: str | None = None,
) -> dict[str, Any]:
"""Add inline comment using PR review comments API."""
pr_info = await self._call_api(f"/repos/{repo}/pulls/{pr_id}")
Expand All @@ -402,14 +437,18 @@ async def add_inline_comment(

commit_sha = pr_info.get("head", {}).get("sha")

data = {
data: dict[str, Any] = {
"body": comment,
"commit_id": commit_sha,
"path": file_path,
"line": line,
"side": "RIGHT" if line_type == "new" else "LEFT",
}

if start_line is not None:
data["start_line"] = start_line
data["start_side"] = "RIGHT" if (start_line_type or line_type) == "new" else "LEFT"

result = await self._call_api(
f"/repos/{repo}/pulls/{pr_id}/comments", method="POST", data=data
)
Expand Down
29 changes: 27 additions & 2 deletions src/code_review_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def extract_related_prs(
},
"line": {
"type": "integer",
"description": "Line number to comment on",
"description": "Line number to comment on (end line for multi-line comments)",
},
"line_type": {
"type": "string",
Expand All @@ -173,6 +173,15 @@ def extract_related_prs(
"type": "string",
"description": "Comment content",
},
"start_line": {
"type": "integer",
"description": "Start line for a multi-line comment (omit for single-line)",
},
"start_line_type": {
"type": "string",
"enum": ["old", "new"],
"description": "Line type for start_line; defaults to line_type if omitted",
},
"host": {
"type": "string",
"description": "GitLab host for self-hosted instances",
Expand Down Expand Up @@ -255,9 +264,21 @@ def extract_related_prs(
"type": "object",
"properties": {
"file_path": {"type": "string"},
"line": {"type": "integer"},
"line": {
"type": "integer",
"description": "Line number to comment on (end line for multi-line comments)",
},
"line_type": {"type": "string", "enum": ["old", "new"]},
"comment": {"type": "string"},
"start_line": {
"type": "integer",
"description": "Start line for a multi-line comment (omit for single-line)",
},
"start_line_type": {
"type": "string",
"enum": ["old", "new"],
"description": "Line type for start_line; defaults to line_type if omitted",
},
},
"required": ["file_path", "line", "line_type", "comment"],
},
Expand Down Expand Up @@ -362,6 +383,8 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
arguments["line"],
arguments["line_type"],
arguments["comment"],
arguments.get("start_line"),
arguments.get("start_line_type"),
)

elif name == "add_pr_comment":
Expand All @@ -388,6 +411,8 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
comment_data["line"],
comment_data["line_type"],
comment_data["comment"],
comment_data.get("start_line"),
comment_data.get("start_line_type"),
)
if res.get("success"):
batch_results["inline_success"] += 1
Expand Down