Appearance
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: Readpermission on the attachment's project (checked via the sameAttachmentPolicy::downloadgate 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
| Parameter | Type | Required | Description |
|---|---|---|---|
attachment_id | integer | Yes | The 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 anyimage/*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 forapplication/json - Returns an error content block (with
download_urlfallback hint) for any other MIME type — PDFs, zips, video, audio, and office documents are not supported in v1 - The
attachment_idis 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 family | Content block type | Payload |
|---|---|---|
image/* | image | data (base64-encoded bytes) + mimeType |
text/*, application/json | text | text (raw file contents, UTF-8) |
| Anything else | text with isError: true | Human-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.
| Type | Default limit | Environment variable |
|---|---|---|
| Images | 5 MB | MCP_ATTACHMENT_MAX_IMAGE_BYTES (bytes) |
| Text / JSON | 500 KB | MCP_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.
| Condition | Error message |
|---|---|
Missing attachment_id argument | Schema validation error from the MCP SDK |
Non-existent attachment_id | Attachment not found |
| Attachment from another tenant | Attachment not found (tenant-scoped lookup) |
| Attachment is orphaned | Attachment is orphaned (not attached to any resource) |
| No permission on the project | You do not have permission to fetch this attachment |
| File row exists but bytes gone from storage | Attachment file is missing from storage — use download_url as a fallback |
| File bytes could not be read | Attachment 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 limit | Attachment too large to fetch ({actual_size}, limit 5 MB) — use download_url as a fallback |
| Text larger than text limit | Attachment 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.ymlattached to the infra migration epic — read it and check whether the postgres service setsPOSTGRES_PASSWORD
Finding Attachment IDs
fetch-attachment takes an ID, not a URL. The following tools and resources expose attachment IDs in their output:
list-reports— each report includes anattachmentsarray with{id, filename, mime_type, size, download_url}kendo://issues/{id}— issue resource includes attachmentskendo://projects/{id}/issues— project issues include attachments