Skip to content

Attachment Tools

One tool for reading attachment bytes so multimodal AI assistants can reason over screenshots, logs, markdown, and JSON directly — instead of only seeing a download_url.

Why a dedicated tool?

Other MCP tools (like list-reports) already expose attachments as metadata with a download_url. That works for non-multimodal clients, but a client that can look at images needs the bytes inline. fetch-attachment hands those bytes back as the right MCP content block type: image for image/*, text for text/* and application/json.

Permission Model

  • Fetching attachments: requires Attachments: Read permission on the attachment's project (checked via the same AttachmentPolicy::download gate used by the web download endpoint)
  • Project owners and admins bypass the permission check
  • Orphaned attachments (not attached to any resource) are rejected before the permission check so tokens with broad access cannot enumerate orphan IDs

fetch-attachment

Fetch the bytes of an attachment by ID and return them as an inline MCP content block. Read-only and safe to call repeatedly.

Parameters

ParameterTypeRequiredDescription
attachment_idintegerYesThe ID of the attachment to fetch bytes for

Behavior

  • Marked as #[IsReadOnly] and #[IsIdempotent] — safe to call multiple times with no side effects
  • Returns an image content block (base64-encoded bytes + mimeType) for any image/* MIME type — PNG, JPEG, WebP, GIF, AVIF, etc.
  • Returns a text content block (raw file contents) for any text/* MIME type (plain, markdown, CSV, HTML, XML) and for application/json
  • Returns an error content block (with download_url fallback hint) for any other MIME type — PDFs, zips, video, audio, and office documents are not supported in v1
  • The attachment_id is tenant-scoped automatically via the tenant database connection — attachments from other tenants are reported as "not found"

Response Shape

The response is a standard MCP tool result wrapping one of three content block types:

MIME familyContent block typePayload
image/*imagedata (base64-encoded bytes) + mimeType
text/*, application/jsontexttext (raw file contents, UTF-8)
Anything elsetext with isError: trueHuman-readable error message (see Errors below)

Size Limits

Size is checked before bytes are read from storage, so oversized attachments reject immediately without loading into memory.

TypeDefault limitEnvironment variable
Images5 MBMCP_ATTACHMENT_MAX_IMAGE_BYTES (bytes)
Text / JSON500 KBMCP_ATTACHMENT_MAX_TEXT_BYTES (bytes)

Limits exist to protect the LLM's context window. If you need to read a larger attachment, use the download_url that other tools expose (e.g. on issues, comments, reports) and fetch it with your Passport bearer token.

Errors

All errors are returned as MCP error content blocks (isError: true) — never as HTTP 4xx/5xx responses. Every error path is recorded in the ai_mcp_logs audit table with the exact error_message.

ConditionError message
Missing attachment_id argumentSchema validation error from the MCP SDK
Non-existent attachment_idAttachment not found
Attachment from another tenantAttachment not found (tenant-scoped lookup)
Attachment is orphanedAttachment is orphaned (not attached to any resource)
No permission on the projectYou do not have permission to fetch this attachment
File row exists but bytes gone from storageAttachment file is missing from storage — use download_url as a fallback
File bytes could not be readAttachment file could not be read — use download_url as a fallback
Unsupported MIME type (PDF, zip, video, etc.)Cannot fetch attachment of MIME type {mime} — use download_url as a fallback
Image larger than image limitAttachment too large to fetch ({actual_size}, limit 5 MB) — use download_url as a fallback
Text larger than text limitAttachment too large to fetch ({actual_size}, limit 500 KB) — use download_url as a fallback

Example Prompts

Show me the screenshot attached to report 23 — is the sidebar collapsing as described?

Open the CSV file attached to issue KD-0482 and tell me how many rows have status "failed"

There's a docker-compose.yml attached to the infra migration epic — read it and check whether the postgres service sets POSTGRES_PASSWORD

Finding Attachment IDs

fetch-attachment takes an ID, not a URL. The following tools and resources expose attachment IDs in their output:

See Also

  • Reports — Reports include attachment metadata that can be fed into fetch-attachment
  • Issues — Issues can carry attachments that become LLM-readable via this tool
  • Resources — Read issue, project, and report data (which includes attachment IDs)