fix: add pending_time to update_ticket and surface article attachments - #287
fix: add pending_time to update_ticket and surface article attachments#287stephaneberle9 wants to merge 6 commits into
Conversation
Setting a ticket to "pending reminder"/"pending close" failed with "Missing required value for field 'pending_time'!" because the tool never exposed that field, which Zammad requires for pending states. Thread pending_time through the param model, client, and tool, and reject pending states without it up front so the LLM gets a clear message instead of a raw API error.
Users reported the connector "only returns text" and could not read PDFs, even though download tooling exists: reading a ticket never revealed that attachments were present, so their ids stayed unknown. The read Article model had no attachments field, so the metadata the Zammad API returns per article was silently dropped (BaseModel ignores extras) and never rendered. Add an attachments field to Article and list each file (id, filename, size) in the markdown ticket/article output with a pointer to zammad_download_attachment, making the existing download path discoverable.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughAdds scheduling support (pending_time) to ticket updates and surfaces article attachments in ticket markdown; includes model changes, client serialization, server rendering/sanitization, docstring examples, and tests. ChangesPending Time and Article Attachment Support
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 4 |
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.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/server.py`:
- Around line 633-640: The attachment metadata is inserted into markdown
unescaped (see the attachments loop using att_id, filename, size_str and the
lines.append call); sanitize/escape these values before formatting: ensure
att_id is safely stringified (no control chars), escape/encode filename to
neutralize Markdown/control characters (implement or call a helper like
escape_markdown or escape_text), and validate/format size into a safe numeric
string, then use those sanitized variables in the lines.append call instead of
raw att.get(...) values.
In `@tests/test_server.py`:
- Around line 1060-1080: Rename the test and update its docstring to reflect
that the server forwards pending_time as a datetime object (not an ISO string):
change the function name test_update_ticket_forwards_pending_time_as_iso to
something like test_update_ticket_forwards_pending_time_as_datetime and update
the docstring in the test function body to state "The tool forwards pending_time
to the client as a datetime object." Keep the existing assertions that check
kwargs["pending_time"] == datetime(..., tzinfo=timezone.utc) and leave the rest
of the test (ZammadMCPServer setup, params, and call to zammad_update_ticket)
unchanged.
🪄 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
Run ID: 8cf68499-61b3-45d6-af04-266e419d1ddb
📒 Files selected for processing (6)
mcp_zammad/client.pymcp_zammad/models.pymcp_zammad/server.pytests/test_client_methods.pytests/test_models.pytests/test_server.py
Add OAuth2 authentication that proxies to Zammad's built-in Doorkeeper provider, forwarding each user's bearer token so the MCP server acts under their own identity (multi-user HTTP deployments). Static ZAMMAD_HTTP_TOKEN remains the default for single-user setups. Adds AuthConfig/OAuthProxy/TransportConfig in config.py, wires per-request client construction in server.py, an HTTP entrypoint in __main__.py, and the supporting env vars, docs, and tests. Stacked on the pending_time/attachment fixes (upstream PR basher83#287); kept as a separate branch so the OAuth PR can follow once those fixes land and Zammad 7.2.0 (zammad/zammad#6034) is released.
Attachment filenames originate from user uploads, so render them through a new _sanitize_inline_text helper that strips control characters and HTML-escapes the result before inserting into markdown. Also only emit a size suffix for genuine non-negative integers to guard the dict path. Rename test_update_ticket_forwards_pending_time_as_iso to ..._as_datetime to reflect that the tool forwards a datetime object (the client performs the ISO conversion), and add coverage for filename sanitization and invalid size handling. Addresses CodeRabbit findings on PR basher83#287.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
mcp_zammad/server.py (1)
651-659:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSanitize
article_idin the attachment section header as well.
article_idis still interpolated raw in the header line, while attachment fields are sanitized. If article payloads come from dicts, this leaves an avoidable markdown/control-text injection path.🔧 Minimal fix
def _format_article_attachments(attachments: list[Attachment] | list[dict] | None, article_id: int) -> list[str]: @@ - lines = [f"- **Attachments** (download via zammad_download_attachment, article_id={article_id}):"] + safe_article_id = _sanitize_inline_text(article_id) + lines = [f"- **Attachments** (download via zammad_download_attachment, article_id={safe_article_id}):"]🤖 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/server.py` around lines 651 - 659, The header line building the attachments section uses raw article_id while other fields are sanitized; update the code that constructs lines (the first element assigned to variable lines) to pass article_id through _sanitize_inline_text (e.g., safe_article_id = _sanitize_inline_text(article_id)) and use that sanitized value in the header string so the attachments header is protected from injection just like the per-attachment fields.
🤖 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.
Duplicate comments:
In `@mcp_zammad/server.py`:
- Around line 651-659: The header line building the attachments section uses raw
article_id while other fields are sanitized; update the code that constructs
lines (the first element assigned to variable lines) to pass article_id through
_sanitize_inline_text (e.g., safe_article_id =
_sanitize_inline_text(article_id)) and use that sanitized value in the header
string so the attachments header is protected from injection just like the
per-attachment fields.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9846f0c5-96b1-4fba-b6d8-95bc5064e240
📒 Files selected for processing (2)
mcp_zammad/server.pytests/test_server.py
Pass article_id through _sanitize_inline_text in the attachments header line for consistency with the per-attachment fields; the dict call-path can yield a non-int id. Addresses CodeRabbit follow-up on PR basher83#287.
Codacy flagged _format_article_attachments at cyclomatic complexity 10 (limit 8). Extract _attachment_field, _format_attachment_size, and _format_attachment_line helpers so each function stays well under the threshold. Behavior is unchanged; covered by existing attachment tests.
Codacy reads .codacy.yaml and ignores the sibling .codacy.yml (when both exist, .yaml wins), so the test exclusions and disabled pydocstyle in .codacy.yml were inert. As a result Codacy applied its default pydocstyle profile to test files and flagged D203 (blank line before class docstring) on a newly added test class -- a rule that conflicts with the D211 convention Ruff enforces project-wide. Mirror the test-exclusion intent into the active .codacy.yaml so the static-analysis gate stops flagging test docstrings.
Heads-up: the Codacy config is shadowed (root cause of the D203 failure here)While fixing the static-analysis failure on this PR, I found the repo has three Codacy config files that contradict each other, and Codacy is reading the wrong one:
When both What I did in this PR (minimal fix): mirrored the Suggested follow-up (out of scope here): consolidate to a single source of truth. Either:
Right now a future edit to |
Attachment filenames originate from user uploads, so render them through a new _sanitize_inline_text helper that strips control characters and HTML-escapes the result before inserting into markdown. Also only emit a size suffix for genuine non-negative integers to guard the dict path. Rename test_update_ticket_forwards_pending_time_as_iso to ..._as_datetime to reflect that the tool forwards a datetime object (the client performs the ISO conversion), and add coverage for filename sanitization and invalid size handling. Addresses CodeRabbit findings on PR basher83#287.
Pass article_id through _sanitize_inline_text in the attachments header line for consistency with the per-attachment fields; the dict call-path can yield a non-int id. Addresses CodeRabbit follow-up on PR basher83#287.
Attachment filenames originate from user uploads, so render them through a new _sanitize_inline_text helper that strips control characters and HTML-escapes the result before inserting into markdown. Also only emit a size suffix for genuine non-negative integers to guard the dict path. Rename test_update_ticket_forwards_pending_time_as_iso to ..._as_datetime to reflect that the tool forwards a datetime object (the client performs the ISO conversion), and add coverage for filename sanitization and invalid size handling. Addresses CodeRabbit findings on PR basher83#287.
Pass article_id through _sanitize_inline_text in the attachments header line for consistency with the per-attachment fields; the dict call-path can yield a non-int id. Addresses CodeRabbit follow-up on PR basher83#287.
Summary
Two independent, self-contained fixes to the ticket read/write tools, each with tests. No new dependencies, no config/transport changes.
1.
feat: add pending_time to update_ticket for pending statesProblem: Moving a ticket to
pending reminder/pending closefailed withMissing required value for field 'pending_time'!.Cause: Zammad requires a
pending_timefor pending states, but the tool (and the client method) never exposed that field.Remedy: Thread
pending_timethroughTicketUpdateParams,ZammadClient.update_ticket, and thezammad_update_tickettool (accepts ISO 8601, serialized for the API). A model validator rejects a pending state submitted withoutpending_timeup front, so the model gets an actionable message instead of a raw API error.2.
fix: surface article attachments when reading ticketsProblem: Attachments/PDFs appeared unreadable — reading a ticket returned "only text", so attachment ids were never known and
zammad_download_attachmentcouldn't be used.Cause: The read
Articlemodel had noattachmentsfield, so the per-article attachment metadata returned by the Zammad API was silently dropped (BaseModelignores extras) and never rendered.Remedy: Add an
attachmentsfield toArticleand list each file (id, filename, size) in the markdown ticket/article output with a pointer tozammad_download_attachment, making the existing download path discoverable.Testing
uv run pytest— 231 passeduv run ruff format --check/ruff check— cleanuv run mypy mcp_zammad— cleanNotes
Branched off
main; contains only these two fixes (an unrelated OAuth feature is intentionally kept out and will be proposed separately).Summary by CodeRabbit
New Features
Behavior
Documentation
Tests