Skip to main content
Glama
capture-and-slack-mcp

capture-and-slack-mcp

by baho0

capture-and-slack-mcp

An MCP server that relays a result screenshot to Slack so you can review a coding agent's output without opening the app.

You hand an agent a task on your desktop app (e.g. Clonify, a Qt/VTK CAD tool), and end it with "…and send me the result via the capture-and-slack MCP." The agent renders the result to an image and this server posts it to your Slack DM or a channel. You glance at Slack instead of launching the app.

This server does not drive any GUI — it only relays images.

How it fits the loop

agent finishes a task
  ├─ (A, preferred)  runs a repro/test that offscreen-renders the result → /tmp/result.png
  │                     → send_to_slack(["/tmp/result.png"], "3mm gaussian emboss done")
  └─ (B, fallback)   the app window is on screen (X11)
                        → capture_window_and_post("result", window="Clonify")
        → Slack files_upload_v2 → you see the image in your Slack DM/channel

Path A is the robust one: the app's own headless render (VTK SetOffScreenRendering(1) + vtkWindowToImageFilter, optionally under QT_QPA_PLATFORM=offscreen) writes a PNG, and this server just posts it. Path B screenshots the live window and is for when no offscreen render exists.

Related MCP server: screenshot-mcp

Tools

Tool

What it does

send_to_slack(images, message="", channel=None, title=None, status=None, checks=None, links=None, thread_ts=None, echo_image=False)

Post image(s) to Slack as a review: a root anchor message (a Block Kit task card when title/status/checks/links are given) with the image(s) threaded beneath it, and 👍/👎 seeded for one-tap review. Returns a JSON status with review_id and the permalink(s). Primary tool.

capture_window_and_post(message="", window=None, channel=None, title=None, trim=True, allow_fullscreen=False, thread_ts=None, echo_image=False)

Screenshot the running window (X11/KDE) and post it as a review. window defaults to CAPTURE_WINDOW_TITLE; use "active" or "full".

capture_window(window=None, trim=True, allow_fullscreen=False)

Screenshot without posting and return it for inspection. Compose with send_to_slack. No Slack token needed.

Safety: when a specific window title can't be matched to an on-screen window, capture returns NO_WINDOW_MATCH rather than silently grabbing the focused window or the whole desktop (which would leak unrelated apps/secrets to Slack). Pass allow_fullscreen=True (or window="full") to opt into the whole-screen fallback deliberately. | wait_for_review(review_id, channel=None, timeout_seconds=50, poll_seconds=5) | Block until the human reacts (👍/👎/👀/✏️) or replies in the review thread. Returns approved/rejected/changes/seen, or still_awaiting on timeout — call again to keep waiting. | | check_review(review_id, channel=None) | Non-blocking single read of the current verdict. Use in your own polling loop. | | ask_review(review_id, question, channel=None, timeout_seconds=50, poll_seconds=5) | Ask the reviewer a follow-up question in Slack (e.g. why they rejected) and wait for their typed answer. Posts + broadcasts the question so an away reviewer sees it. Returns answered+reply, or still_awaiting+ask_id. | | wait_for_reply(review_id, since_ts, channel=None, timeout_seconds=50, poll_seconds=5) | Block until a thread reply appears after since_ts (e.g. the ask_id from ask_review). Returns answered+reply or still_awaiting. |

Image paths must be absolute (the server's working directory differs from the agent's). Errors come back as a JSON envelope {"code","message","hint"} in an isError result.

Closing the review loop

send_to_slack returns a review_id (the thread's root message ts). The agent seeds a task card, the human taps 👍/👎 on their phone (or replies "make it 5mm"), and wait_for_review(review_id) returns the verdict to the agent:

send_to_slack(["/tmp/result.png"], title="3mm emboss", status="pass",
              checks={"volume Δ": "+2.1%", "watertight": "yes"})   → {"review_id": "1712…", …}
wait_for_review("1712…", channel="C…")                            → {"status": "approved", …}

Re-render into the same thread by passing thread_ts=review_id to send_to_slack; add notify=True to broadcast the re-render back to the channel (🔁) so a watching reviewer is re-pinged.

If the agent needs to ask something (e.g. the reviewer rejected and the agent wants to know why), it must ask in Slack, not in the terminal — the reviewer is watching Slack. ask_review posts the question into the thread, pings the reviewer, and returns their typed answer:

wait_for_review("1712…", "C…")                                   → {"status": "rejected"}
ask_review("1712…", "Why — emboss too sharp, or wrong face?")    → {"status": "answered", "reply": "too sharp"}

Requirements

  • uv and Python ≥ 3.12.

  • A Slack bot token (see Slack setup — image uploads need files_upload_v2; webhooks can't attach files).

  • For live capture only: an X11 session with ImageMagick import or KDE spectacle installed. Precise per-window targeting uses xdotool if present, else xwininfo (already on most X11 desktops) — no install needed. Match by window title or WM_CLASS substring; a class like QClonifyApp disambiguates from an editor tab that merely has "Clonify" in its title.

Slack setup (one time)

  1. Go to https://api.slack.com/apps → Create New App → From an app manifest, pick your workspace, and paste:

    {
      "display_information": { "name": "Capture and Slack Bot" },
      "features": { "bot_user": { "display_name": "capture-and-slack", "always_online": true } },
      "oauth_config": { "scopes": { "bot": [
        "files:write", "chat:write", "im:write",
        "reactions:write", "reactions:read",
        "channels:history", "groups:history", "im:history"
      ] } },
      "settings": { "org_deploy_enabled": false, "socket_mode_enabled": false }
    }

    Scopes: files:write (upload), chat:write (the caption/card), im:write (open a DM when the destination is a user ID). For the review loop: reactions:write (seed 👍/👎), reactions:read (read the verdict), and history (channels:history for public channels, groups:history for private, im:history for DMs) to read threaded replies. Drop the loop scopes if you only ever post one-way. If you add scopes to an existing app, reinstall it so the new grants take effect.

  2. Install to Workspace → authorize → copy the Bot User OAuth Token (xoxb-…) from OAuth & Permissions.

  3. Pick a destination ID:

    • DM to yourself: your member ID U… (Slack profile → ⋮ → Copy member ID). The bot can DM you without an invite.

    • A channel: the channel ID C… (channel → View details), and run /invite @capture-and-slack in it.

  4. Provide the token to the server (do not commit it): cp .env.example .env and fill in SLACK_BOT_TOKEN and SLACK_DEFAULT_CHANNEL, or export them in your shell.

  5. (Optional, for precise window targeting) sudo pacman -S xdotool.

Install & test

uv sync
uv run pytest

Register with Claude Code

Project-scoped (.mcp.json is already in this repo):

{
  "mcpServers": {
    "capture-and-slack": {
      "command": "uv",
      "args": ["run", "--directory", "/home/baho/Desktop/capture-and-slack-mcp", "capture-and-slack-mcp"]
    }
  }
}

The server reads SLACK_BOT_TOKEN / SLACK_DEFAULT_CHANNEL from the environment or a git-ignored .env. (Keep the token out of .mcp.json, which is committed.)

Configuration

Env var

Default

Purpose

SLACK_BOT_TOKEN

—

Bot token (xoxb-…). Required to post.

SLACK_DEFAULT_CHANNEL

—

Default destination: U… (DM), C… (channel), or D… (DM channel).

CAPTURE_WINDOW_TITLE

Clonify

Default window title substring for live captures.

CAPTURE_SLACK_MAX_FILE_MB

25

Soft cap: larger images are auto-shrunk (downscale → JPEG) to fit before uploading.

CAPTURE_SLACK_HARD_MAX_MB

200

Hard ceiling: files bigger than this are rejected (FILE_TOO_LARGE) instead of shrunk.

CAPTURE_SLACK_SCRATCH_DIR

<tmp>/capture_and_slack_mcp

Where captured/shrunk PNGs are written (created 0700, files 0600).

CAPTURE_SLACK_SCRATCH_TTL_MIN

60

Reap scratch files older than this (minutes) on access; 0 disables.

CAPTURE_SLACK_KEEP_SCRATCH

(unset)

Set (1/true) to never delete/reap scratch files — for debugging.

LOG_LEVEL

INFO

Server log level, written to stderr (DEBUG, INFO, WARNING, …).

SLACK_MAX_RETRIES

3

Auto-retries for rate-limited (429, honors Retry-After) / connection-error Slack calls.

SLACK_HTTP_TIMEOUT

30

Per-request Slack HTTP timeout (seconds).

Development

Module map — only server.py imports the MCP SDK:

  • server.py — the 3 tools + main().

  • slack_client.py — slack_sdk wrapper: files_upload_v2, DM resolution, error translation.

  • capture.py — X11/KDE window capture. Layered fallback: resolve window id (xdotool → xwininfo) and grab it directly → active window (spectacle) → fullscreen.

  • images.py — Pillow: path/content validation, auto-trim, PNG byte reads.

  • config.py — env-driven settings (with a tiny .env loader).

  • errors.py — ErrorCode + CaptureSlackError (JSON envelope).

License

MIT

Available Tools

7 tools
ask_reviewA

Ask the reviewer a follow-up question IN SLACK and wait for their typed answer. Posts the question into the review thread and pings the channel so an away reviewer (watching Slack, not your terminal) actually sees it — ALWAYS use this instead of asking in the console. Returns the answer (status 'answered'), or 'still_awaiting' + ask_id on timeout; then keep waiting with wait_for_reply(review_id, since_ts=ask_id).

Inputs: question (required), review_id (required), channel, poll_seconds, +1 more.

capture_windowA

Capture a screenshot WITHOUT posting, and return it for inspection. Compose with send_to_slack: look at the image, then post the returned path if it's the right frame. No Slack call, so no token/scopes are needed.

Inputs: trim, window, allow_fullscreen.

capture_window_and_postA

Screenshot the running app window (X11/KDE) and post it to Slack as a review. Use only when no offscreen render exists; otherwise prefer send_to_slack with a repro-rendered PNG.

Inputs: trim, title, window, channel, +4 more.

check_reviewA

Non-blocking: read the current review verdict once and return immediately (status approved / rejected / changes / seen / still_awaiting). Use in your own polling loop; use wait_for_review to block until the human responds.

Inputs: review_id (required), channel.

send_to_slackA

Post one or more existing image files to Slack as a review. Posts a root anchor message (a Block Kit task card when title/status/checks/links are given) and threads the image(s) beneath it. Returns a JSON status including review_id (poll it with wait_for_review) and the Slack permalink(s). This is the primary tool: pair it with a repro/test that renders the result to a PNG.

Inputs: images (required), links, title, checks, +6 more.

wait_for_replyA

Block until the reviewer posts a thread reply AFTER since_ts, or until timeout. Returns the reply text (status 'answered') or 'still_awaiting'. Use to keep waiting after ask_review timed out.

Inputs: since_ts (required), review_id (required), channel, poll_seconds, +1 more.

wait_for_reviewA

Block until the human reacts (👍/👎/👀/✏️) or replies in the review thread, or until timeout. Returns a JSON verdict: status is approved / rejected / changes / seen, or 'still_awaiting' on timeout (with a hint to call again). Pair with send_to_slack's review_id.

Inputs: review_id (required), channel, poll_seconds, timeout_seconds.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.4/5.0

    Scored across 7 tools

    Disambiguation5/5

    Each tool has a clear, distinct purpose: capture without posting, capture and post, send existing files, ask questions, check status, and two blocking wait variants. No two tools overlap in functionality.

    Naming Consistency5/5

    All tool names use snake_case with a consistent verb_noun pattern (ask_review, capture_window, check_review, send_to_slack, wait_for_reply, wait_for_review). The slightly longer 'capture_window_and_post' still follows the same structure and is clear.

    Tool Count5/5

    With 7 tools, the server is well-scoped for its purpose: capturing screenshots and managing Slack review workflows. Each tool adds necessary functionality without bloat or redundancy.

    Completeness4/5

    The tool set covers the main workflow: capture, post, review, follow-up. A minor gap is the lack of a tool to post a regular Slack message without a review, but the core review-focused flow is well-supported.

    Related MCP Connectors

    Related MCP Servers