Attach images returned by MCP tools #2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "slash/mcp-image-attachments"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Deployed to CT#137 as
2.2.0+c126a1d3and confirmed working end to end.The problem
The MCP client extracted only
TextContent, so image blocks were dropped and the agent posted an unreachable markdown URL instead of the picture.The design that matters
Images are posted from the MCP tool resolver — the single point every tool call passes through — not handed to the conversation layer.
The first version did the latter, and it was wrong. Tools execute from at least six places (mention, DM, approval, tool follow-up, regeneration, and the non-post runner), each needing to remember to consume a stash. Three rounds of fixes each uncovered another path that did not, and every miss failed silently: tool succeeded, model described the picture, nothing posted. Diagnostics on the running server caught the last one directly — an image captured with no consumer.
Posting from the resolver means a new caller cannot miss it.
llm.Context.ResponseRootIDcarries the thread; a path that forgets to set it posts to the channel rather than losing the image, so the failure mode is visible instead of silent.Also fixed along the way
httpservice.RequestTimeout). SDXL takes ~40s warm, sogenerate_imagefailed every time. MCP now has its own client with a 10 minute ceiling — stillMakeClient(false), soAllowedUntrustedInternalConnectionsis still enforced..jfif.mime.ExtensionsByTypereturns alphabetically, soimage/jpegyielded an extension Mattermost will not preview. Types are mapped explicitly, with a test pinning it.Review notes
mmapi.ClientgainsUploadFile; mocks regenerated with mockery v3.2.5.Operational gotchas (in slash-deploy RUNBOOK)
The MCP client extracted only TextContent from tool results, so image blocks were silently discarded. Skynet's generate_image returns both an inline image and a markdown URL; the agent therefore posted only the URL, which points at a tailnet address no user's browser can reach — every generated image rendered broken. Images cannot ride back through a tool result, because that value is text fed to the model. They also cannot be attached to the streamed reply: that post is created empty before streaming starts, and Mattermost does not allow attachments to be added to an existing post on update. So images are carried out-of-band and posted as a threaded follow-up. - mcp/client.go: collect ImageContent alongside text. An image-only result is no longer an error ("no text content found"), since that is a legitimate response. - mcp/tool_images.go: stash images on llm.Context.Parameters, mirroring the existing web-search pattern. Unlike that one this is a true consume — the stash is cleared so a second read cannot post the same picture twice. - mcp/user_clients.go: stash from the tool resolver; when a tool returns only an image, give the model a short text note so it knows what happened. - conversations/handle_messages.go: after streaming, upload the images and post them in the thread. Upload failures are logged, not returned — the text answer has already been delivered and losing a picture should not fail the reply. - mmapi: add UploadFile (thin wrapper over pluginAPI.File.Upload); mocks regenerated with mockery v3.2.5. Not yet verified end to end against a real generate_image call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>