Skip to main content
Glama
baho0

capture-and-slack-mcp

by baho0
README.md
# 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.

## 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`](https://docs.astral.sh/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:
   ```json
   {
     "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

```bash
uv sync
uv run pytest
```

## Register with Claude Code

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

```json
{
  "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

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.

Maintenance

ActivityStale
ResponsivenessNo issues