Skip to content

feat: add zammad_merge_tickets tool via legacy ticket_merge REST endpoint - #312

Open
iisat wants to merge 2 commits into
basher83:mainfrom
iisat:feat/merge-tickets-tool
Open

feat: add zammad_merge_tickets tool via legacy ticket_merge REST endpoint#312
iisat wants to merge 2 commits into
basher83:mainfrom
iisat:feat/merge-tickets-tool

Conversation

@iisat

@iisat iisat commented Aug 2, 2026

Copy link
Copy Markdown

Summary

Implements #309 — adds a zammad_merge_tickets tool that merges one Zammad ticket into another, the same operation as the "Merge" button in the Zammad UI.

API choice

The tool wraps the legacy endpoint:

PUT /api/v1/ticket_merge/{source_ticket_id}/{target_ticket_number}

Rationale:

  • zammad_py does not expose ticket merge, so the client performs a raw authenticated request reusing the existing session/credentials.
  • The newer PUT /api/v1/tickets/{id}/merge route (with target_ticket_number in the body) returns 404 on some released Zammad versions, while ticket_merge is the route the web UI itself uses.

Behavior

  • Target by number or ID: target_ticket accepts the display ticket number; an internal ticket ID can be passed via target_ticket_id instead (its number is looked up first, since the endpoint requires the number).
  • Failure detection: Zammad reports a failed merge as HTTP 200 with {"result": "failed"} — the tool raises ValueError in that case instead of reporting a false success.
  • Safety: the tool description warns that merge is irreversible (all articles move to the target, the source is set to state merged) and that both customers may see the merged conversation. The tool is annotated as non-read-only.

Test plan

  • uv run pytest --cov=mcp_zammad — 234 passed, coverage 88.27% (gate: 86%)
  • uv run ruff format --check mcp_zammad tests / uv run ruff check mcp_zammad tests — clean
  • uv run mypy mcp_zammad — clean
  • New tests: 6 client cases (success, HTTP-200-with-result:"failed", HTTP errors, target lookup) + 6 model validation cases
  • Verified against a production Zammad instance: merged duplicate monitoring tickets, articles moved, source set to merged

Closes #309

Summary by CodeRabbit

  • New Features

    • Added a ticket merge tool for combining a source ticket into a target ticket using either its number or ID.
    • Merging moves all articles to the target ticket and is irreversible.
    • Added validation to ensure valid ticket identifiers and clear handling of missing tickets or failed merges.
  • Documentation

    • Documented the ticket merge tool and its irreversible behavior.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@iisat, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9e67a042-80d8-4ec1-a28e-8939d16e6733

📥 Commits

Reviewing files that changed from the base of the PR and between 1314c94 and 1d543d5.

📒 Files selected for processing (6)
  • mcp_zammad/client.py
  • mcp_zammad/models.py
  • mcp_zammad/server.py
  • tests/test_client_methods.py
  • tests/test_models.py
  • tests/test_server.py

Walkthrough

Changes

Ticket merge flow

Layer / File(s) Summary
Merge contracts and validation
mcp_zammad/models.py, tests/test_models.py
Adds merge parameter and result models. Validates positive IDs, non-empty ticket numbers, and exactly one target identifier.
Client merge request
mcp_zammad/client.py, tests/test_client_methods.py
Adds target-number and target-ID merge flows through the legacy endpoint. Tests validation, HTTP errors, and failed results.
MCP tool integration
mcp_zammad/server.py, README.md
Registers zammad_merge_tickets, maps missing source tickets to guidance errors, and documents the irreversible merge behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: basher83

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title uses conventional commit format and clearly describes the new Zammad ticket merge tool and endpoint.
Linked Issues check ✅ Passed The implementation exposes the requested Zammad merge endpoint, supports the required ticket identifiers, handles failures, and adds tests and warnings.
Out of Scope Changes check ✅ Passed The client, models, server tool, documentation, and tests directly support the ticket merge feature without unrelated changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added type:bug Something is not working correctly area:mcp-tools area:security Security and policy work area:ci-cd Continuous integration and deployment pipelines area:python Python development and tooling area:web APIs, web services, or UI work labels Aug 2, 2026
@codacy-production

codacy-production Bot commented Aug 2, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 7 complexity

Metric Results
Complexity 7

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@iisat
iisat force-pushed the feat/merge-tickets-tool branch from 2c2fbb1 to d8268ce Compare August 2, 2026 07:41
@github-actions github-actions Bot added the type:security Security-related work label Aug 2, 2026
…oint

Wraps PUT /api/v1/ticket_merge/{source_id}/{target_number} (zammad_py does
not expose merge; the newer PUT /tickets/{id}/merge route 404s on some
instances). Target can be given by display number or internal ID (number
is looked up). Failed merges (HTTP 200 + result='failed') raise ValueError
instead of being reported as success.

Tests: 6 client + 6 model cases; suite 236 passed.
@iisat
iisat force-pushed the feat/merge-tickets-tool branch from d8268ce to 1314c94 Compare August 2, 2026 07:50
@iisat
iisat marked this pull request as ready for review August 2, 2026 12:20
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
coderabbitai Bot requested a review from basher83 August 2, 2026 15:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mcp_zammad/client.py`:
- Line 300: Validate target_ticket_number in TicketMergeParams against the
expected ticket-number format, preferably digits only, before it is used by the
ticket merge request. Ensure the validation rejects path separators, traversal
sequences, and other nonconforming values while preserving valid ticket numbers;
alternatively, URL-encode the value at the request construction near the
session.put call.
- Around line 296-298: Make target-ticket lookup failures distinguishable in
mcp_zammad/client.py lines 296-298 by handling
self.api.ticket.find(target_ticket_id) and raising an error that identifies the
target. In mcp_zammad/server.py lines 1227-1233, inspect that error before
calling _handle_ticket_not_found_error and report target-related failures using
target_ticket_id or target_ticket_number; retain source_ticket_id for
source-related failures.

In `@tests/test_client_methods.py`:
- Around line 655-744: Add a test to TestMergeTickets covering target-ticket
lookup failure: configure mock_instance.ticket.find to raise the expected
exception for target_ticket_id, call merge_tickets with that ID, and assert the
exception type and message identify the missing target ticket distinctly from
source-ticket merge failures. Verify the merge HTTP endpoint is not called.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c6255243-b61f-4e62-a3f7-364df2081159

📥 Commits

Reviewing files that changed from the base of the PR and between 8873b2e and 1314c94.

📒 Files selected for processing (6)
  • README.md
  • mcp_zammad/client.py
  • mcp_zammad/models.py
  • mcp_zammad/server.py
  • tests/test_client_methods.py
  • tests/test_models.py

Comment thread mcp_zammad/client.py
Comment thread mcp_zammad/client.py
target = self.api.ticket.find(target_ticket_id)
target_ticket_number = str(target["number"])

response = self.api.session.put(f"{self.url}/ticket_merge/{source_ticket_id}/{target_ticket_number}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validate target_ticket_number before it reaches the URL path.

target_ticket_number is only constrained to length 1-50 in TicketMergeParams; it is not restricted to a specific character set. It is interpolated directly into f"{self.url}/ticket_merge/{source_ticket_id}/{target_ticket_number}" with no URL-encoding. A crafted value containing / or .. changes the request path sent through the authenticated session.

Restrict the field to the expected ticket-number format (e.g. digits) in the model, or URL-encode the value with urllib.parse.quote before building the request path.

As per path instructions for mcp_zammad/client.py, "Check for URL validation to prevent SSRF attacks."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp_zammad/client.py` at line 300, Validate target_ticket_number in
TicketMergeParams against the expected ticket-number format, preferably digits
only, before it is used by the ticket merge request. Ensure the validation
rejects path separators, traversal sequences, and other nonconforming values
while preserving valid ticket numbers; alternatively, URL-encode the value at
the request construction near the session.put call.

Source: Path instructions

Comment thread tests/test_client_methods.py
iisat added a commit to iisat/Zammad-MCP that referenced this pull request Aug 2, 2026
- Distinguish target-ticket lookup failures from source-ticket errors:
  wrap the ticket.find() call in client.merge_tickets with a ValueError
  identifying the TARGET ticket, and report it against target_ticket_id
  in the server tool instead of always blaming source_ticket_id.
- Restrict TicketMergeParams.target_ticket_number to digits only
  (pattern ^\d+$) since the value is interpolated into the ticket_merge
  URL path without encoding.
- Add tests: non-digit target numbers rejected, target lookup failure
  is distinguishable, and error attribution (source vs target) in the
  merge tool.
- Distinguish target-ticket lookup failures from source-ticket errors:
  wrap the ticket.find() call in client.merge_tickets with a ValueError
  identifying the TARGET ticket, and report it against target_ticket_id
  in the server tool instead of always blaming source_ticket_id.
- Restrict TicketMergeParams.target_ticket_number to digits only
  (pattern ^\d+$) since the value is interpolated into the ticket_merge
  URL path without encoding.
- Add tests: non-digit target numbers rejected, target lookup failure
  is distinguishable, and error attribution (source vs target) in the
  merge tool.
@iisat
iisat force-pushed the feat/merge-tickets-tool branch from 1d47157 to 1d543d5 Compare August 2, 2026 16:17
@iisat

iisat commented Aug 2, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:ci-cd Continuous integration and deployment pipelines area:python Python development and tooling area:security Security and policy work area:web APIs, web services, or UI work type:bug Something is not working correctly type:security Security-related work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: ticket merge tool (PUT /api/v1/ticket_merge)

1 participant