Inspiration Slack is where modern organizations talk — and for millions of people using screen readers, low vision, or reading in a second language, a lot of that conversation quietly falls apart. An image posted with no alt-text is announced as just "image." A 600-character paragraph with no structure becomes one flat, unskippable wall of audio. An acronym nobody defined stops a new teammate cold.

Accessibility tooling almost always lives after the fact — a linter in your CI, an audit before a launch. But workplace communication is real-time and social, and nobody wants to be publicly corrected. We asked: what if accessibility was a quiet, helpful nudge at the moment of posting — private to the author, one click to fix, and smart enough to respect how people actually work?

That's AccessAlly: real-time accessibility for Slack, built for the Slack Agent for Organizations track.

What it does When someone posts a message or image, the matching detector fires and the author gets a private, ephemeral nudge with a one-click fix — no channel spam, no public shaming. Three detectors ship today, mapped to real WCAG success criteria:

Missing alt-text (WCAG 1.1.1) — vision AI writes alt-text; Approve reposts an accessible version. Walls of text (WCAG 1.3.1) — AI restructures the message with headings and bullets. Undefined jargon (WCAG 3.1.3) — Slack's Real-time Search API finds where a teammate already explained the term and links it.

Plus a per-channel accessibility score, /a11y commands, a native App Home dashboard with a workspace-wide compliance rollup, admin-controlled monitoring, a weekly digest, and CSV compliance export.

How we built it AccessAlly is a fully async Python app on Slack Bolt (Socket Mode) — no public URL needed. The design goal was that every detector is the same shape, so the product is a framework, not three bolted-together checks:

message / file_shared → Bolt handler (acks <3s, work in a background task) → Detector (protocol) → DetectionResult → Groq (vision + text) · Slack Real-time Search · MCP rules server → ephemeral Block Kit nudge + SQLite score store Two of the challenge's three eligible technologies do real work here:

AccessAlly's own MCP server (Model Context Protocol, stdio transport) serves the WCAG rulebook, an org glossary, and per-channel policy as tools. Because the ruleset is a pluggable MCP component, an organization can point AccessAlly at its own MCP server exposing an internal style guide — and the same agent enforces both public WCAG standards and house norms. One agent, your policies. The Real-time Search API (assistant.search.context) powers the jargon detector's "someone already explained this" moment — semantic, and permission-aware, so results respect the author's own access. The scoring model is deliberately simple and lives in stdlib SQLite. A channel's score over a rolling 7-day window is:

$$\text{score} = \left\lfloor 100 \times \left(1 - \frac{\text{issues_found}}{\max(\text{checks_run},, 1)}\right) \right\rceil$$

bucketed as $\ge 90$ 🟢, $70\text{–}89$ 🟡, and $< 70$ 🔴.

Everything degrades gracefully: Groq, RTS, MCP, and Slack failures are caught, logged, and fall back — they never crash a handler. A two-layer error-defense suite (35 tests) locks that in.

What we learned Semantic search will always return something. Our first jargon build treated any search hit as "prior definition found" — and cheerfully quoted a completely unrelated message as the definition of an invented term. We learned to gate on relevance: a hit is only quoted if it actually defines the term. Confident-but-wrong is worse than silent. Great detectors are mostly restraint. The jargon extractor over-fired on ordinary words like runbooks and failover. The wins came from what not to flag — an acronym-shape gate, a stoplist, and skipping terms the author already defined inline. Ack first, always. Slack's 3-second rule is unforgiving; every handler acks instantly and does the real work in a background task. Augment, don't edit. Slack won't let an app edit a user's uploaded file, so Approve alt-text reposts an accessible version instead of mutating the original — a platform constraint we turned into a cleaner trust story.

Challenges we faced The "app did not respond" ghost. A whole debugging session chased a dead /a11y command and a jargon detector that suddenly "missed" obvious acronyms. The code was fine — macOS had put the laptop to sleep, silently killing the Socket Mode connection for 73 minutes. The fix was one word: caffeinate -is. The lesson (check that your socket is alive before blaming your handlers) is now in our README. One upload, two events. A single image fires both file_shared and a file_share message — so we nudged twice. A TTL dedupe guard keyed on file_id fixed it. Block Kit's 2000-character button limit. A restructured wall of text is far too big to stash in a button's value, so we park fix payloads in a bounded in-memory registry and hand the button a short id. Verifying, not guessing, Slack's newest APIs. The Real-time Search API is new and moved from a single search:read scope to granular ones; we verified every endpoint and scope against docs.slack.dev and smoke-tested live rather than trusting memory. What's next Colour-contrast analysis, audio/video transcription, and multilingual support — each is a new detector against the same contract, so the app, nudges, scoring, and commands pick it up unchanged. Accessibility that ships where you already work.

Built With

  • a11y
  • accessibility
  • assistant-search-context
  • asyncio
  • block-kit
  • groq
  • httpx
  • llama-3.3-70b
  • llama-4-scout
  • mcp
  • model-context-protocol
  • openai-compatible-api
  • pydantic
  • pytest
  • python
  • python-dotenv
  • real-time-search
  • slack
  • slack-api
  • slack-bolt
  • socket-mode
  • sqlite
  • stdlib-http-server
  • vision-ai
  • wcag
Share this project:

Updates