feat(cli, mcp): first-class image & file attachments on issues + comments
Closes #173 (closed).
Problem
The CLI's four issue/comment commands (create, update, post-comment, update-comment) accept text only. Adding a screenshot to a comment is a 2-step manual dance: upload, copy markdown, paste into a follow-up command. The MCP server is worse — it has zero file tools, so AI agents using the postgresai MCP can post text but cannot attach screenshots, EXPLAIN diagrams, flame graphs, or any other file.
The plumbing existed (uploadFile, downloadFile, buildMarkdownLink in lib/storage.ts) but was not wired into the comment/issue surfaces.
What this MR does
CLI: new repeatable --attach <path> flag
| Command | Behavior |
|---|---|
pgai issues create --attach foo.png |
Upload, append link to --description (or set description to just the link if --description omitted). |
pgai issues post-comment <id> "msg" --attach foo.png |
Upload, append to the comment body. |
pgai issues update-comment <id> "new content" --attach foo.png |
Upload, append to <content>. |
pgai issues update <id> --attach foo.png |
If --description present, append to it. If omitted, fetch the existing description and append — so "add a screenshot to issue X" is one round-trip. |
Repeatable: pass --attach multiple times; links go on consecutive lines preserving order. Image extensions (.png/.jpg/.jpeg/.gif/.webp/.svg/.bmp/.ico) render inline (![]()); other types render as plain links ([]()).
MCP: new tools + attachments parameter
- New
upload_file({path})tool — returns URL + ready-to-paste markdown link. - New
download_file({url, output_path?})tool — symmetric download. - New optional
attachments: string[](local file paths) onpost_issue_comment,create_issue,update_issue,update_issue_comment. Server-side does upload-then-append. Forupdate_issuewithattachmentsand nodescription, fetches existing description and appends.
Shared helpers (CLI ↔️ MCP)
lib/storage.ts gets two new helpers used by both surfaces:
uploadAttachments({apiKey, storageBaseUrl, attachmentPaths})— sequential upload, returns per-file{path, url, markdown, metadata}. Sequential (not parallel) so error messages can pinpoint which file failed.appendAttachmentsToContent(content, attachments)— appends one link per line with a two-newline separator. No-op on empty input. Doesn't strip user newlines.
Both surfaces calling the same helper means the CLI flag and the MCP attachments parameter cannot drift in output format.
Tests
30 new tests added (all passing locally):
storage.test.ts(10 new) —uploadAttachmentsround-trip with mock fetch + real tmp files,appendAttachmentsToContentedge cases (empty content, whitespace-only, multi-link ordering, trailing newlines), partial-upload error semantics.issues.cli.test.ts(9 new) — fake storage/uploadendpoint added to the existing fake-API harness; covers single-file, multi-file, image-vs-non-image rendering,update --attachfetch-and-append vs append-to-supplied-description, missing-file abort (no comment-create reaches the server), plain-flow regression check.mcp-server.test.ts(11 new) —upload_file,download_file,attachmentson each issue/comment tool, attachments-only-no-content (allowed), no-fields validation rejects (mentions attachments), update_issue fetch-then-append ordering.
Existing assertions touched: two error messages widened from "content is required" to "content or attachments is required" to reflect the new "attachments alone is fine" semantics.
bun test final tally on this branch: 820 pass, 32 skip, 8 fail — the 8 failures all pre-exist on origin/main (Docker/network-dependent mon local-install tests + one checkup-api HTTP-allow test); confirmed by stashing my changes and re-running.
Live smoke tests
Tested end-to-end against the postgres-ai team org on production (orgId=4). Test issue 019ddad3-6249-7aaa-a000-b093a5af1094 (now closed) covered:
| Test | Result |
|---|---|
create --attach <png> --attach <log> |
![]() (inline image) + []() (plain link) |
post-comment --attach <png> |
|
update-comment --attach <png> --attach <log> |
is_edited=True, content shows new text + 2 links in order |
update --attach <log> (no --description) |
|
issues files upload standalone |
|
issues files download round-trip |
cmp confirms downloaded PNG bytes match original |
Test plan
-
bun test test/storage.test.ts test/issues.cli.test.ts test/mcp-server.test.tspasses locally - CI (
test:fast) passes; the pre-existing 8 failures are unrelated to this MR - After merge:
pgai issues post-comment <id> \"hi\" --attach screenshot.pngworks end-to-end against staging - After merge: an MCP-using agent (e.g. Claude Code with postgresai MCP) sees
upload_file/download_fileintools/list
Compatibility
- All flags / parameters are additive and optional. Existing scripts and MCP clients keep working unchanged.
- Two error messages widened (now mention attachments). Anyone matching the exact string
\"content is required\"from the MCP tool error path will need to update — unlikely, but flagging.