Skip to content

feat: bulk fetch tools (full ticket, attachments, inline images) - #47

Open
RajRoR wants to merge 1 commit into
effytech:mainfrom
RajRoR:feat/bulk-fetch-tools
Open

RajRoR wants to merge 1 commit into
effytech:mainfrom
RajRoR:feat/bulk-fetch-tools

Conversation

@RajRoR

@RajRoR RajRoR commented May 12, 2026

Copy link
Copy Markdown

Summary

Adds four tools aimed at "ticket archaeology" workflows where an agent needs the entire ticket history without manual pagination, plus a small pyproject.toml cleanup.

New tools

  • get_ticket_full — ticket + ALL conversations (paginated, no truncation), plus optional requester/agent expansion and status-label decoding. Returns an attachments_index covering both ticket-level and per-conversation attachments.
  • download_ticket_attachments — parallel download of every attachment to disk under <dest_dir>/<ticket_id>/, with a per-file size cap (default 50 MB).
  • extract_inline_images — resolves cid: refs against attachments and downloads remote <img src> URLs from the description and every conversation body. Handles data: URIs too.
  • decode_ticket_status — int → label using ticket_fields metadata (covers custom statuses).

These have proven valuable in agent workflows where the model needs to reason over an entire ticket including embedded screenshots, without the user manually paginating conversations or matching cid: refs to attachment URLs by hand.

Cleanup

  • Drops build>=1.2.2.post1 from runtime dependencies — it's a build-time tool, not a runtime requirement, and ships ~1MB of extra dependencies into every install.

Behavior unchanged

  • Existing get_ticket / get_ticket_conversation are untouched.
  • New tools reuse parse_link_header for pagination.
  • Auth model unchanged (env vars).

Implementation notes

  • Helpers (_fd_get, _fetch_all_conversations, _load_status_map) are private and named with underscore prefix to keep them out of the tool surface.
  • Status map is cached per process via _STATUS_CACHE.
  • Downloads stream via httpx and enforce a size limit before writing.
  • Inline image extraction uses stdlib html.parser (no new dep).

Caveat for hosts

get_ticket_full output for busy tickets routinely exceeds 256 KB / 25 k tokens. README notes this and recommends slicing the persisted result with jq.

Test plan (verified against live Freshdesk)

  • uv build produces sdist + wheel cleanly
  • freshdesk-mcp boots via uvx --from . freshdesk-mcp (env-only, exit 0)
  • get_ticket_full returns 25-conversation ticket with status_label='Action Plan Provided', attachments_index populated
  • download_ticket_attachments runs end-to-end (count_saved/count_errors reported; ticket under test had 0 file attachments, only inline)
  • extract_inline_images resolves 6 inline <img> URLs to disk; non-zero file sizes verified
  • Size cap enforced — calling extract_inline_images(size_limit_mb=0) produces 6 file exceeds size_limit error entries and 0 saved files
  • decode_ticket_status(12)Action Plan Provided; decode_ticket_status(99999) → fallback Custom(99999)

🤖 Generated with Claude Code

Adds four tools aimed at "ticket archaeology" workflows where an
agent needs the entire ticket history without manual pagination:

- get_ticket_full: ticket + ALL conversations (paginated, no
  truncation) plus optional requester/agent expansion and status
  label decoding. Returns an attachments_index covering both
  ticket-level and per-conversation attachments.
- download_ticket_attachments: parallel download of every
  attachment to disk under <dest_dir>/<ticket_id>/, with a
  per-file size cap.
- extract_inline_images: resolves cid: refs against attachments
  and downloads remote <img src> URLs from the description and
  every conversation body. Handles data: URIs too.
- decode_ticket_status: int -> label using ticket_fields metadata
  (covers custom statuses).

Existing get_ticket / get_ticket_conversation behavior is
unchanged. New tools share helpers (_fd_get,
_fetch_all_conversations, _load_status_map) and the existing
parse_link_header for pagination.

Also drops `build>=1.2.2.post1` from runtime dependencies — it's a
build-time tool, not a runtime requirement, and ships ~1MB of
extra dependencies into every install.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
mpasternak added a commit to mpasternak/freshdesk_mcp that referenced this pull request Aug 16, 2026
Ticket bodies come from whoever emailed the helpdesk, so anything derived
from them is attacker-influenced input. The bulk fetch tools merged from
upstream PR effytech#47 treated it as trusted.

- extract_inline_images fetched any http(s) URL found in an <img src>. A
  customer could mail <img src="http://169.254.169.254/latest/meta-data/">
  and have the server request it, write the response to disk and hand the
  path to the model. URLs harvested from ticket HTML are now checked against
  loopback, link-local, private, reserved, multicast and unspecified ranges,
  and non-http(s) schemes are refused. Redirects are followed one hop at a
  time and re-checked, because automatic redirect following would let a
  public URL bounce to 127.0.0.1. Attachment URLs issued by Freshdesk are
  exempt - they are not attacker-controlled.
- Conversation paging looped until Freshdesk stopped advertising a next page.
  It now stops at FRESHDESK_MAX_CONVERSATION_PAGES (default 50) and reports
  conversations_truncated instead of silently implying it read everything.
- Attachment downloads were fired all at once through asyncio.gather. They now
  run behind a semaphore (FRESHDESK_DOWNLOAD_CONCURRENCY, default 5) and stop
  at FRESHDESK_MAX_DOWNLOAD_FILES (default 200), reporting skipped_over_limit.
- Downloads defaulted to /tmp/fd, a shared path any local account can read or
  pre-create. The default is now a per-user directory created mode 0700.

Tests cover the URL checks, the refusal to issue a request for a blocked
address, and the paging cap. Both workflows run them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ytwHf2AcXX4ZzN4Zd6c6i
@mpasternak

Copy link
Copy Markdown

@RajRoR I merged this branch into a fork I run against a live Freshdesk — the bulk fetch tools are useful, get_ticket_full especially. While reviewing the merge I found a few spots where ticket content drives more behaviour than intended, so I've opened RajRoR#1 against your branch with fixes and tests rather than leaving it as a comment. Short version:

  • extract_inline_images will fetch any URL a customer puts in an <img src>. A support email containing <img src="http://169.254.169.254/latest/meta-data/"> makes the server request it, write the response to disk and return the path. The patch rejects URLs resolving to loopback / link-local / private / reserved ranges and non-http(s) schemes, re-checking each redirect hop (follow_redirects=True would otherwise let a public URL bounce to 127.0.0.1). cid: references point at Freshdesk's own attachment URLs and stay exempt.
  • _fetch_all_conversations pages without an upper bound — one call costs whatever someone wrote into the ticket. Capped, and truncation is now reported instead of implied.
  • Attachment downloads all start at once via asyncio.gather, each allowed 50 MB. Now semaphore-limited with a file cap.
  • /tmp/fd is world-readable and pre-creatable on a shared host. Default moved to a per-user directory, mode 0700.

9 tests included, verified against this branch as-is on MCP SDK 1.4.1. The patch is SDK-independent, so it doesn't collide with #50.

Merge it, cherry-pick it, or rewrite it your way — no ownership implied, it's your PR. Findings came from an automated security review and were then verified by hand.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants