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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,7 @@ The MCP server checks properties in this order. Each property is resolved indepe
- `delete_participants` - Remove participants from a review
- `add_comment` - Add a comment to a review
- `reply_comment` - Reply to an existing comment
- `edit_comment` - Edit a comment's body and/or task state (open/addressed/verified; author only)
- `append_change` - Add a changelist to an existing review
- `replace_with_change` - Replace review content with a changelist
- `join` - Join a review as a participant
Expand Down
9 changes: 9 additions & 0 deletions p4mcp/handlers/review_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,15 @@ def require(attr, label=None):
params.body
)

elif action == "edit_comment":
require("comment_id", "comment_id")
result = await self.review_services.edit_comment(
params.comment_id,
getattr(params, "body", None),
getattr(params, "task_state", None),
getattr(params, "notify", None)
)

elif action == "append_change":
require("review_id")
require("change_id", "change_id")
Expand Down
18 changes: 16 additions & 2 deletions p4mcp/models/review_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class ReviewModifyAction(str, Enum):
APPEND_PARTICIPANTS = "append_participants"
ADD_COMMENT = "add_comment"
REPLY_COMMENT = "reply_comment"
EDIT_COMMENT = "edit_comment"
APPEND_CHANGE = "append_change"
REPLACE_WITH_CHANGE = "replace_with_change"
JOIN = "join"
Expand All @@ -162,6 +163,8 @@ class FixStatus(str, Enum):
class TaskState(str, Enum):
OPEN = "open"
COMMENT = "comment"
ADDRESSED = "addressed"
VERIFIED = "verified"

class NotifyMode(str, Enum):
IMMEDIATE = "immediate"
Expand Down Expand Up @@ -393,7 +396,8 @@ class ModifyReviewsParams(BaseParams):
)
task_state: Optional[TaskState] = Field(
default=None,
description="Task state (optional for add_comment)",
description="Task state (optional for add_comment: open|comment; "
"edit_comment additionally accepts addressed|verified)",
examples=["open"]
)
notify: Optional[NotifyMode] = Field(
Expand Down Expand Up @@ -442,7 +446,7 @@ def need(field: Any, label: Optional[str] = None):
raise ValueError(f"{label or field} is required for action: {a}")

# Actions requiring review_id
if a not in [ReviewModifyAction.CREATE, ReviewModifyAction.ARCHIVE_INACTIVE] and a != ReviewModifyAction.CREATE:
if a not in [ReviewModifyAction.CREATE, ReviewModifyAction.ARCHIVE_INACTIVE, ReviewModifyAction.EDIT_COMMENT] and a != ReviewModifyAction.CREATE:
if a not in [ReviewModifyAction.ARCHIVE_INACTIVE] and not self.review_id:
raise ValueError(f"review_id is required for action: {a}")

Expand All @@ -464,6 +468,16 @@ def need(field: Any, label: Optional[str] = None):
elif a == ReviewModifyAction.ADD_COMMENT:
need("review_id")
need("body", "body")
if self.task_state and self.task_state not in [TaskState.OPEN, TaskState.COMMENT]:
raise ValueError(
"task_state must be 'open' or 'comment' for add_comment; "
"'addressed'/'verified' are only reachable via edit_comment"
)

elif a == ReviewModifyAction.EDIT_COMMENT:
need("comment_id", "comment_id")
if not self.body and not self.task_state:
raise ValueError("At least one of body or task_state is required for edit_comment action")

elif a == ReviewModifyAction.REPLY_COMMENT:
need("review_id")
Expand Down
43 changes: 42 additions & 1 deletion p4mcp/services/review_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,48 @@ async def reply_to_comment(
except Exception as e:
logger.error(f"Failed to reply to comment '{comment_id}' in review '{review_id}': {e}")
return {"status": "error", "message": str(e)}


async def edit_comment(
self,
comment_id: int,
body: Optional[str] = None,
task_state: Optional[str] = None,
notify: Optional[str] = None,
) -> Dict[str, Any]:
"""POST /api/v11/comments/{id}/edit - Edit a comment body and/or its task state

Only fields provided are updated. Swarm only allows the comment's author
to edit it (403 otherwise). task_state accepts
"comment"|"open"|"addressed"|"verified". Swarm's docs describe the flow
open -> addressed -> verified, but live testing against Swarm (API v11)
showed the server does not enforce the ordering; treat it as the
recommended convention rather than a hard constraint.

Args:
comment_id = 1234
body = "Updated comment text."
task_state = "addressed"
notify = "delayed"|"immediate"
"""
try:
auth = await self._get_auth()
api_base = await self._get_api_base()
url = f"{api_base}/comments/{comment_id}/edit"

payload = {}
if body is not None:
payload["body"] = body
if task_state:
payload["taskState"] = task_state
if notify:
payload["notify"] = notify

r = requests.post(url, auth=auth, json=payload, verify=self.verify_ssl)
return {"status": "success", "message": self._handle_response(r)}
except Exception as e:
logger.error(f"Failed to edit comment '{comment_id}': {e}")
return {"status": "error", "message": str(e)}

async def append_change_to_review(
self,
review_id: int,
Expand Down
14 changes: 9 additions & 5 deletions p4mcp/tools/review_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ async def modify_reviews(
action: Annotated[Literal[
"create", "refresh_projects", "vote", "transition",
"append_participants", "add_comment", "reply_comment",
"append_change", "replace_with_change", "join",
"edit_comment", "append_change", "replace_with_change", "join",
"archive_inactive", "mark_comment_read", "mark_comment_unread",
"mark_all_comments_read", "mark_all_comments_unread",
"update_author", "update_description",
Expand Down Expand Up @@ -252,20 +252,24 @@ async def modify_reviews(
)] = None,
body: Annotated[Optional[str], Field(
default=None,
description="Comment body (required for add_comment, reply_comment)",
description="Comment body (required for add_comment, reply_comment; optional for edit_comment)",
examples=["Looks good."],
)] = None,
task_state: Annotated[Optional[Literal["open", "comment"]], Field(
task_state: Annotated[Optional[Literal["open", "comment", "addressed", "verified"]], Field(
default=None,
description="Task state",
description="Task state. add_comment accepts only 'open'|'comment'; "
"edit_comment additionally accepts 'addressed'|'verified'. The documented "
"flow is open -> addressed -> verified, though Swarm does not necessarily "
"enforce the ordering server-side; only the comment author can edit",
)] = None,
notify: Annotated[Optional[Literal["immediate", "delayed"]], Field(
default=None,
description="Notification mode",
)] = None,
comment_id: Annotated[Optional[int], Field(
default=None,
description="Parent comment ID (reply_comment, mark_comment_read/unread)",
description="Comment ID (target of edit_comment, parent for reply_comment, "
"mark_comment_read/unread)",
examples=[987],
)] = None,
not_updated_since: Annotated[Optional[str], Field(
Expand Down