Slack Canvas
MCP server and skill that let agents keep a structured Slack channel canvas (open topics, closed-topic History, on-disk archive).
Install
openclaw plugins install clawhub:slack-canvas-mcpSlack Canvas
An MCP server (plus a short agent skill) that lets AI agents keep a Slack channel's canvas as a readable notebook of topics. Works with OpenClaw, Claude Code and any MCP client.
Each configured channel gets one canvas:
# FENCE REPAIR
**Latest Update** (2026-01-12 09:30)
Quote from two installers; the cheaper one can start next week.
**Next Steps**
- Book the cheaper installer (owner: you)
---
# Full Context
## FENCE REPAIR
Both quotes, sources, reasoning ...
---
# HISTORY (closed topics)
- **GARDEN SHED** (closed 2026-01-05): Picked the cedar kit.
Open topics sit on top. A closed topic becomes one line in the History section; its full text stays on disk (and, if you want, in a Markdown archive file your agent's memory can search). Slack's free plan allows one canvas per channel and no standalone canvases, so History is a section of the channel canvas, not a second canvas.
Tools
| Tool | What it does |
|---|---|
canvas_list(channel_id) | Canvas id, open and closed topic counts |
canvas_create(channel_id, name) | Make sure the channel canvas exists (main or History; idempotent) |
canvas_update(channel_id, topic, latest_update, next_steps, full_context="", canvas="main") | Add or replace one topic, rewrite the canvas. canvas="History" writes a closed topic |
canvas_read(channel_id, canvas="main") | The canvas as Slack shows it; History returns the full closed-topic archive |
canvas_move_topic(channel_id, topic, from_canvas, to_canvas) | main to History closes (stamps the date); back to main reopens |
canvas_delete_topic(channel_id, topic, canvas="main") | Remove a topic for good |
Topic state lives on disk and is the source of truth; every change rewrites the whole canvas
with one canvases.edit call. If Slack fails, the change is kept and shows on the next update.
The server cannot tell which agent calls it: your MCP host decides which agents get the tools.
Slack setup
- Create a Slack app (https://api.slack.com/apps) or reuse your bot's app.
- Bot token scopes:
canvases:write- create the channel canvas and edit itchannels:read- look up a public channel's existing canvas (conversations.info)groups:read- the same for private channels (only if you use private channels)
- Install the app to the workspace and copy the bot token (
xoxb-...). - Invite the bot to each channel it should write to.
Install
Needs Python 3.10+ and uv on the machine that runs the server.
uv reads the dependency list from the script header (mcp>=1.30,<2) and caches it; nothing is
installed globally. Without uv: pip install 'mcp>=1.30,<2' and run the script with python3.
OpenClaw (from ClawHub or a local copy):
openclaw plugins install clawhub:slack-canvas-mcp # or: openclaw plugins install ./clawhub-slack-canvas
The plugin declares the slack-canvas stdio server and the slack-canvas skill. Tools appear
to agents as slack-canvas__canvas_update and so on. The token and config path come from the
gateway's environment, or set them on the server entry in openclaw.json
(${VAR} references are expanded):
{
mcp: { servers: { "slack-canvas": {
command: "uv",
args: ["run", "--quiet", "--script", "/path/to/plugin/server/slack_canvas_mcp.py"],
env: { SLACK_BOT_TOKEN: "${SLACK_CANVAS_BOT_TOKEN}", SLACK_CANVAS_CONFIG: "/path/to/config.json" }
} } }
}
An entry under mcp.servers with the same name replaces the plugin's default, so this is
also how you point at a uv that is not on the gateway's PATH.
Claude Code: the folder is also a Claude plugin (.claude-plugin/plugin.json, .mcp.json).
Or add the server by hand:
claude mcp add slack-canvas -e SLACK_BOT_TOKEN=xoxb-... -- uv run --quiet --script /path/to/server/slack_canvas_mcp.py
Any other MCP client: run uv run --script server/slack_canvas_mcp.py as a stdio server.
Configuration
Environment:
| Variable | Default | Meaning |
|---|---|---|
SLACK_BOT_TOKEN | (required) | Bot token used for every channel without its own token_env |
SLACK_CANVAS_CONFIG | $SLACK_CANVAS_HOME/config.json | Channel config (below) |
SLACK_CANVAS_HOME | $XDG_STATE_HOME/slack-canvas-mcp, else ~/.local/state/slack-canvas-mcp | State: topics per channel (state/), cached canvas ids (canvas-ids) |
SLACK_CANVAS_HTTP_TOKEN | (HTTP mode only) | Shared secret, at least 32 characters |
SLACK_CANVAS_DRY | unset | 1 = make no Slack calls (tests) |
The token is read from the environment only; the server never reads token files.
Channel config (config.example.json). Only listed channels can be used, so an agent cannot
write to an arbitrary channel:
{
"channels": {
"CXXXXXXXXXX": { "name": "home-projects" },
"CYYYYYYYYYY": {
"name": "research",
"token_env": "SLACK_BOT_TOKEN_RESEARCH",
"archive_dir": "/path/to/agent-workspace/memory/history",
"archive_index": "/path/to/agent-workspace/MEMORY.md"
}
}
}
| Key | Meaning |
|---|---|
name | Title for a new canvas (<name> notebook) and the archive file name. Default: the channel id |
token_env | Read this channel's token from another variable (one bot per channel) |
archive_dir | Optional. Also write the full closed-topic archive to <archive_dir>/<name>.md on every History change, for example into an agent's memory folder so memory search finds closed topics |
archive_index | Optional, with archive_dir. Append a one-line pointer to the archive to this file once (for example the agent's MEMORY.md) |
The config is re-read on every call; edits apply without a restart. State and archive files are written with mode 600 in 700 directories.
Stdio and HTTP modes
Stdio (default): uv run --script server/slack_canvas_mcp.py. The MCP host starts one
process per session. Simple, but each session pays for its own Python process.
HTTP: one long-lived server for many sessions, MCP streamable HTTP on loopback:
export SLACK_CANVAS_HTTP_TOKEN=$(openssl rand -hex 32)
uv run --script server/slack_canvas_mcp.py --http --port 8792 # binds 127.0.0.1
Every request must carry Authorization: Bearer $SLACK_CANVAS_HTTP_TOKEN; anything else gets
401. The server refuses to start without a token of at least 32 characters (exit 78), so a
loopback port is never open to every local process. It is stateless (no MCP session outlives a
request), so restarts cost nothing, and tool calls run in worker threads so one slow Slack call
does not block other sessions. Writes are serialised with a file lock. Keep --host on
loopback unless you put TLS in front of it.
OpenClaw entry for HTTP mode:
{
mcp: { servers: { "slack-canvas": {
url: "http://127.0.0.1:8792/mcp",
transport: "streamable-http",
headers: { Authorization: "Bearer ${SLACK_CANVAS_HTTP_TOKEN}" }
} } }
}
Run it under your service manager (systemd user unit, launchd) with the same environment.
Tests
Offline; no Slack calls, no network, temp directories only:
uv run --with 'mcp>=1.30,<2' python tests/test_canvas.py # every tool, archive option, tokens; ends "all passed"
uv run --with 'mcp>=1.30,<2' python tests/test_http.py # HTTP auth, tools over HTTP, concurrent writes; ends "http passed"
Limits
- POSIX only (uses
fcntlfile locks): Linux and macOS. - The canvas is rewritten whole on every change; edits made by hand in Slack are overwritten. Treat the canvas as output, the state files as input.
canvases.editis Slack rate-limit tier 3 (50+ calls per minute).
License
MIT, see LICENSE.
