Private issue attachments

Keep screenshots and files on their issue, without a public image host or Git branch.

Create an issue with a screenshot

  1. Call create_issue once. Retain its issue UUID and identifier.
  2. Generate one UUID uploadId for each file and retain it until the upload is confirmed.
  3. Send actual padded base64 bytes to upload_issue_attachment (up to 1 MiB decoded), or call prepare_issue_attachment and POST actual bytes to its binary API path (up to 4 MiB).
  4. Check the returned attachment metadata, or list_issue_attachments, before reporting success. Reopening the issue shows its attachments and full-size image preview.
{
  "name": "upload_issue_attachment",
  "arguments": {
    "issueId": "ENGIN-123",
    "uploadId": "91cd2994-bc77-4417-8dc0-3899c5a6fb65",
    "name": "screenshot.png",
    "contentType": "image/png",
    "data": "<actual canonical padded base64 of your image>"
  }
}

The data placeholder is not a file. A remote MCP server cannot read a path on your machine, fetch arbitrary URLs, or automatically receive a conversation image. Your client must have access to the original bytes and send them. If it cannot, use the CLI or Velocity's paste/file-picker controls. Preparation returns instructions; it does not reserve or upload a file.

Attach a local image with the CLI

Both velocity and vel support these commands after ordinary browser sign-in. No LLM key is required.

velocity login
velocity --workspace your-workspace attachments upload ENGIN-123 \
  --file ./screenshot.png \
  --upload-id 91cd2994-bc77-4417-8dc0-3899c5a6fb65
velocity --workspace your-workspace attachments list ENGIN-123
velocity --workspace your-workspace attachments download ATTACHMENT_UUID --output ./download.png
velocity --workspace your-workspace attachments remove ATTACHMENT_UUID --issue ENGIN-123

Binary stdin uses --file - --name screenshot.png --type image/png. Downloads support --output - and never overwrite an existing file. See the CLI reference.

Binary API

OperationRequest
Upload and associatePOST /api/attachments?workspaceId=UUID&issueId=ENGIN-123&id=UPLOAD_UUID
List ready attachmentsGET /api/attachments?workspaceId=UUID&issueId=ENGIN-123
Private preview/downloadGET /api/attachments/WORKSPACE_UUID/ATTACHMENT_UUID; add ?download=1 to force download
Remove an ordinary attachmentDELETE /api/attachments/WORKSPACE_UUID/ATTACHMENT_UUID

POST a raw binary body, not JSON or multipart form data. Set Content-Type and percent-encode the filename in X-Attachment-Name. Authorization is header-only. prepare_issue_attachment returns the exact path, filename/type headers and selected workspace slug, without a credential or signed storage URL. The 200/201 upload response completes association; no separate finalize call is needed.

# Python 3: credentials stay in memory and out of shell arguments.
import getpass, pathlib, urllib.parse, urllib.request, uuid
token = getpass.getpass("Authorized Velocity token: ")
upload_id = str(uuid.uuid4())  # retain this for explicit retries
params = urllib.parse.urlencode({"workspaceId": "WORKSPACE_UUID",
    "issueId": "ENGIN-123", "id": upload_id})
path = pathlib.Path("screenshot.png")
if not 0 < path.stat().st_size <= 4 * 1024 * 1024:
    raise ValueError("Choose a nonempty file up to 4 MiB")
request = urllib.request.Request("https://velocity.quest/api/attachments?" + params,
    data=path.read_bytes(), method="POST", headers={
        "Authorization": "Bearer " + token,
        "X-Workspace-Slug": "your-workspace",
        "Content-Type": "image/png",
        "X-Attachment-Name": urllib.parse.quote(path.name, safe="")})
print("Upload ID:", upload_id)
with urllib.request.urlopen(request) as response:
    print(response.read().decode())  # metadata only, never file bytes or credentials

Credentials and workspace boundaries

  • MCP connection keys and MCP OAuth grants use mcp:read for reads and mcp:write for writes. Writes cannot use a read-only grant. These credentials require an MCP-eligible plan, live membership and the exact authorized workspace; disabled connections and expired/revoked grants are refused.
  • Ordinary first-party management CLI OAuth retains cli:read/cli:write access in its approved, selected workspace. Ordinary API keys retain the existing admin REST access; unrelated narrow API scopes gain no general REST permission.
  • Browser/JWT access follows live membership and session revocation. Draft previews belong to the uploader. The same existing issue-editing roles apply; attachments do not grant workspace administration.
  • Managed agent executions use the policy-checked MCP tools. Their binary API handoff cannot bypass run policy; direct MCP media writes revalidate their issue scope immediately before the write.

Private retrieval and limits

get_issue and get_issue_context expose ready attachment metadata. list_issue_attachments orders ready files by creation time then ID, up to the 50-file issue limit. Metadata includes ID, filename, MIME type, byte size, issue ID, inline flag, creation time and private API URL.

get_issue_attachment returns metadata by default. Set includeImage: true for a PNG/JPEG/WebP up to 1 MiB as an MCP image content block. Larger images and ordinary files use the authenticated binary URL. URLs require the bearer credential or an authorized browser session; a Markdown link alone does not authorize a download.

PNG, JPEG and WebP declarations must match the actual file signature. Other files are download-only application/octet-stream; HTML/SVG are never rendered inline. The existing limits are 4 MiB per file, 50 files per issue and 1 GiB per workspace. Files and image content remain private and uncached; do not paste bytes or secrets into issue descriptions, comments, prompts or logs.

Retries, drafts and removal

An attachment upload changes no description text. For a partial failure, report the created issue ID, successful attachment IDs and failed upload IDs. List confirmed files before retrying an ambiguous response; explicitly retry the same bytes, filename, type and upload ID. Confirmed same-ID retries do not create duplicates. Changed content requires a new UUID, as does a removed upload; clients never automatically recreate the issue.

For browser drafts, POST with draftId=UUID instead of issueId. The existing issue-description transaction associates validated draft images atomically on save. Unclaimed drafts expire after 24 hours. Ordinary MCP uploads associate directly to an existing issue.

remove_issue_attachment and DELETE remove ordinary files through the durable cleanup queue. An image embedded in the description returns a conflict: remove it through the browser or the version-checked CLI command to preserve concurrent edits. Removing an issue, abandoned drafts and storage failures use the existing durable cleanup lifecycle.

Errors distinguish bad input (400), missing authorization (401), missing issue/file (404), identity/inline/quota conflicts (409), oversized bytes (413) and temporarily unavailable storage (503). MCP reports tool failures with isError; successful preparation alone is never attachment success.