Daily Standup Digest Bot MCP Server
by SaahasK18
README.md
# Daily Standup Digest Bot
An LLM-powered Slack bot that collects standup replies over DM,
summarizes them with an OpenRouter free model, and posts a
consolidated digest to a team channel — with a Model Context Protocol
(MCP) surface that exposes the same capabilities as standardized
tools for AI agents.
---
## Demo overview
The bot lives in a Slack workspace and follows a predictable weekday
cadence:
1. Every weekday morning it DMs each active team member and asks for
their standup update.
2. It listens for DM replies and stores them in SQLite.
3. If someone hasn't responded, it sends **one** gentle follow-up DM.
4. Later that morning it summarizes every reply with an LLM
(OpenRouter free model), highlights real blockers, lists
non-responders, and posts a formatted digest to a designated Slack
channel.
The same workflows are also exposed as MCP tools so an external LLM
agent can list team members, kick off a run, record replies, list
non-responders, and generate a digest — all without touching Slack.
> **Synthetic-data-only demo.** This project ships with sample
> members and sample replies. Sample Slack IDs are prefixed
> `SAMPLE_` on purpose. Do not point it at real customer data.
---
## Problem being solved
Distributed teams spend meaningful time on standups. Written
standups in a channel are a good middle ground, but they suffer from:
- **Scattered replies.** People post at different times; nobody sees
the whole picture without scrolling.
- **Buried blockers.** The one message with the actual blocker is
next to the "no updates" ones.
- **Nagging.** Someone has to manually chase people who forgot.
- **Summarization tax.** A human ends up rewriting the day's replies
into a readable digest.
This bot automates the boring bits (collection, chasing,
summarization, formatting) so humans keep the parts that matter
(actually unblocking each other).
---
## Core workflow
The bot embodies a small **take in → make sense of → act on** loop:
1. **Take in data.** Slack Bolt (Socket Mode) receives DM replies as
`message` events and persists them to SQLite via `db.save_response`.
2. **Make sense of it.** For each raw reply, `agent.summarize_update`
asks the OpenRouter free model to return strict JSON with
`yesterday` / `today` / `blockers`, guided by three reusable
Markdown skill files.
3. **Act on it.** `agent.build_digest` assembles a Slack-formatted
message that lifts real blockers to the top, groups updates by
person, and lists non-responders at the bottom. `app.post_digest`
posts it to the configured `TEAM_CHANNEL_ID`.
The scheduler (`scheduler.py`) drives the same three moments
(`start_standup` → `send_followups` → `post_digest`) automatically on
weekdays.
---
## Features
- **Slack Bolt + Socket Mode.** No public HTTPS endpoint needed for
the Slack app to receive events; the bot connects outbound to
Slack.
- **Slash commands.** `/join-standup`, `/start-standup`,
`/send-standup-followups`, `/post-standup-digest`,
`/schedule-status`, and (guarded) `/reset-test-standup`.
- **DM reply capture.** A `@app.event("message")` listener saves any
DM from an active team member as their standup reply.
- **SQLite state.** Three-table schema (`team_members`,
`standup_runs`, `responses`) with parameterized queries. The path
is overridable via `STANDUP_DB_PATH` (used by tests).
- **APScheduler weekday cron.** `BackgroundScheduler` with three
`CronTrigger` jobs (`day_of_week="mon-fri"`), timezone-aware,
`coalesce=True`, `max_instances=1`, misfire grace period. Also a
fast interval-based test scheduler for local dev.
- **OpenRouter free-model summarization.** `openrouter_client.py` is
a thin wrapper around `openai.OpenAI` pointed at
`https://openrouter.ai/api/v1`; model comes from `OPENROUTER_MODEL`
and defaults to a free slug. Temperature is pinned low (0.1) for
determinism.
- **Reusable Markdown skills.** `skills/summarize_standup.md`,
`skills/detect_blockers.md`, `skills/format_digest.md` are the
instructions the LLM sees, loaded from paths derived from
`__file__`. The same skill files are exposed through MCP as a
read-only resource.
- **One-time follow-up tracking.** A `followup_sent` column ensures
each person is only nudged once per run.
- **MCP surface.** `mcp_server.py` exposes six tools, one resource,
and one prompt via the official `mcp` Python SDK v1 over
Streamable HTTP.
- **LLM-powered MCP agent client.** `mcp_agent.py` discovers the
server's tools dynamically, hands them to OpenRouter as
OpenAI-compatible function tools, and lets the model plan calls
(up to 5 tool-call rounds).
- **Test suite + CI.** 54 pytest cases run offline in under a
second; GitHub Actions runs them on Python 3.12 and 3.13.
- **Synthetic-data safeguards.** Sample IDs prefixed `SAMPLE_`, a
guarded `/reset-test-standup` command, and an MCP
`reset_synthetic_standup` tool that requires `confirm=true`.
---
## Architecture
```mermaid
flowchart LR
subgraph Slack["Slack workspace"]
SU["Team members"]
end
subgraph BotProc["Bot process (app.py)"]
SB["Slack Bolt / Socket Mode"]
SCHED["APScheduler weekday cron"]
end
subgraph MCPProc["MCP server process (mcp_server.py)"]
MCP["FastMCP<br/>Streamable HTTP"]
end
subgraph Core["Shared core"]
SVC["standup_service.py<br/>(business logic)"]
DB[("SQLite<br/>standup.db")]
AG["agent.py<br/>build_digest / summarize_update"]
SK["skills/*.md"]
end
OR["OpenRouter API<br/>(free model)"]
AGENT["mcp_agent.py<br/>LLM-driven CLI"]
SU <--> SB
SB --> SVC
SCHED --> SVC
MCP --> SVC
SVC --> DB
SVC --> AG
AG --> SK
AG --> OR
AGENT --> MCP
AGENT --> OR
```
Slack, the scheduler, and MCP all funnel into `standup_service.py`
so business logic is defined once. The MCP agent talks to the MCP
server and to OpenRouter — it never touches Slack directly.
---
## Technology stack
| Layer | Choice |
| --- | --- |
| Language | Python 3.12 / 3.13 |
| Slack | `slack_bolt` + `slack_sdk` in Socket Mode |
| Persistence | SQLite via the stdlib `sqlite3` module |
| LLM | OpenRouter (free model), called through the `openai` SDK |
| Scheduling | `APScheduler` 3.x (`BackgroundScheduler` + `CronTrigger`) |
| MCP | Official `mcp` Python SDK v1 (`FastMCP`, Streamable HTTP) |
| Config | `python-dotenv` (`.env`) |
| Testing | `pytest` |
| CI | GitHub Actions on Ubuntu, Python 3.12 & 3.13 |
---
## Project structure
```text
.
├── app.py # Slack Bolt app: slash commands, DM listener, scheduler wiring
├── db.py # SQLite schema and low-level queries
├── agent.py # LLM summarization + digest formatter
├── openrouter_client.py # Thin OpenRouter chat-completions client
├── scheduler.py # Weekday cron + local interval test scheduler
├── standup_service.py # Slack-free business logic reused by Slack, cron, and MCP
├── mcp_server.py # MCP tools, resource, and prompt (FastMCP)
├── mcp_agent.py # LLM-driven MCP agent client
├── test_schedule.py # Runnable local synthetic scheduler harness
├── test_mcp_tools.py # Runnable local synthetic service harness
├── test_mcp_client.py # Runnable local MCP connectivity test
├── skills/ # Markdown instruction files (loaded by agent + MCP resource)
├── sample_data/ # Synthetic team members and sample replies
├── tests/ # pytest suite (isolated, offline)
├── requirements.txt
├── .env.example
├── pytest.ini
├── .github/workflows/test.yml # CI: py_compile + pytest on 3.12 and 3.13
├── LICENSE # MIT
├── CONTRIBUTING.md
├── SECURITY.md
└── README.md
```
---
## Slack setup
1. Create a Slack app (from scratch) at
[api.slack.com/apps](https://api.slack.com/apps) and install it in
a dev workspace you own.
2. Enable **Socket Mode**. Generate an App-Level Token with the
`connections:write` scope → this is `SLACK_APP_TOKEN` (starts with
the app-level prefix).
3. Under **OAuth & Permissions**, add these Bot Token Scopes:
- `chat:write` — post messages
- `im:history` — read DMs the bot is a party to
- `im:read`, `im:write` — open/read DMs
- `commands` — slash commands
- `users:read` (optional, for nicer display names)
4. Under **Event Subscriptions → Subscribe to bot events**, add
`message.im`.
5. Under **Slash Commands**, create these six commands. Request URL
can be anything valid — Socket Mode ignores it. Point them at:
`/join-standup`, `/start-standup`, `/send-standup-followups`,
`/post-standup-digest`, `/schedule-status`, `/reset-test-standup`.
6. Install the app to your dev workspace and copy the Bot User OAuth
Token → `SLACK_BOT_TOKEN`.
7. Invite the bot to a private test channel and copy the channel ID
into `TEAM_CHANNEL_ID`.
---
## Environment variables
Copy `.env.example` to `.env` and fill in your own values. **Never
commit `.env`**; it is gitignored.
| Variable | Purpose | Default |
| --- | --- | --- |
| `SLACK_BOT_TOKEN` | Bot User OAuth token (starts with the bot prefix). | — (required for `app.py`) |
| `SLACK_APP_TOKEN` | App-level token with `connections:write` for Socket Mode. | — (required for `app.py`) |
| `TEAM_CHANNEL_ID` | Channel where the digest is posted. | — (required for digest posting) |
| `OPENROUTER_API_KEY` | OpenRouter API key. | — (required for summarization / digest / MCP agent) |
| `OPENROUTER_MODEL` | OpenRouter model slug. | `openrouter/free` |
| `STANDUP_TIME` | HH:MM (24h) when the standup DMs go out. | `09:00` |
| `FOLLOWUP_TIME` | HH:MM for the one-time follow-up. | `09:45` |
| `DIGEST_TIME` | HH:MM for posting the digest. | `10:15` |
| `SCHEDULE_TIMEZONE` | IANA timezone for cron. | `America/Los_Angeles` |
| `ENABLE_SCHEDULER` | Truthy (`true` / `1` / `yes` / `on`) turns on APScheduler. | `false` |
| `ALLOW_TEST_RESET` | Truthy value enables `/reset-test-standup`. | `false` |
| `MCP_SERVER_URL` | Endpoint the MCP client tools connect to. | `http://localhost:8000/mcp` |
| `STANDUP_DB_PATH` | Override the SQLite path (used by tests). | `standup.db` |
---
## Running the Slack bot
```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# edit .env with your own dev credentials — never commit real values
python3 app.py
```
You should see the bot connect via Socket Mode, and (if
`ENABLE_SCHEDULER=true`) a summary of the scheduled weekday jobs.
Try it out inside Slack:
- Run `/join-standup` in any channel where the bot is present to add
yourself to the synthetic team.
- Run `/start-standup`. The bot DMs every active member and creates
pending response rows.
- Reply to the DM with a short update. The bot confirms with
`✅ Thanks! Your test standup update was saved.`
- Run `/send-standup-followups` to nudge anyone who hasn't replied.
- Run `/post-standup-digest` to build the AI-assisted digest and post
it to `TEAM_CHANNEL_ID`.
---
## Running the MCP server
```bash
python3 mcp_server.py
```
The server listens at:
```
http://localhost:8000/mcp
```
It uses the `streamable-http` transport from the official MCP Python
SDK v1. The Slack app and APScheduler are **not** started by
`mcp_server.py`.
Explore the surface with MCP Inspector:
```bash
npx -y @modelcontextprotocol/inspector
```
Set the transport to **Streamable HTTP** and point it at
`http://localhost:8000/mcp`.
You can also drive the service layer directly (no MCP, no Slack) as
a smoke test:
```bash
python3 test_mcp_tools.py
```
---
## Running the MCP agent
`mcp_agent.py` is an LLM-driven CLI that connects to the MCP server,
lists tools, hands them to OpenRouter as OpenAI-compatible function
tools, and lets the model plan calls (up to 5 tool-call rounds).
```bash
# terminal 1
python3 mcp_server.py
# terminal 2 — one-shot request via CLI argument
python3 mcp_agent.py "List the synthetic standup team."
# or interactive prompt
python3 mcp_agent.py
# Enter an agent request: Who has not responded today?
```
The agent will never post to Slack — MCP tools in this project do
not send Slack messages by design.
---
## Testing
The pytest suite runs entirely offline: no real Slack, no real
OpenRouter, no real filesystem outside of pytest's `tmp_path`.
```bash
pytest -q
```
Under the hood:
- The `temp_db` fixture points the app at a per-test SQLite file via
`STANDUP_DB_PATH` and calls `init_db()` for a fresh schema.
- An autouse fixture wipes Slack / OpenRouter env vars so no test can
accidentally use real credentials.
- `agent.call_llm` is monkeypatched in every test that hits the LLM
path.
CI (`.github/workflows/test.yml`) runs a compile-check across all
production modules followed by `pytest -q` on Ubuntu with both
Python 3.12 and 3.13.
Full recommended local checklist:
```bash
python3 -m py_compile app.py db.py agent.py openrouter_client.py scheduler.py standup_service.py mcp_server.py mcp_agent.py
pytest -q
git status
```
---
## Available Slack commands
| Command | Behavior |
| --- | --- |
| `/join-standup` | Add the invoking user to the synthetic standup team. |
| `/start-standup` | Create today's run, DM every active member, and record pending responses. |
| `/send-standup-followups` | DM one follow-up reminder to each nonresponder who hasn't already been nudged. |
| `/post-standup-digest` | Build the AI-assisted digest and post it to `TEAM_CHANNEL_ID`. |
| `/schedule-status` | Report whether automatic scheduling is enabled and show the configured times / timezone. |
| `/reset-test-standup` | **Synthetic-only.** Delete today's run and responses (team members preserved). Guarded by `ALLOW_TEST_RESET`. |
---
## Available MCP tools
All tools delegate to `standup_service.py`, so their behavior mirrors
the Slack and scheduler paths.
| Tool | What it does |
| --- | --- |
| `list_team_members` | Return active synthetic team members. |
| `start_standup_run` | Create today's run and pending response rows. **Does not send Slack DMs.** |
| `record_standup_reply` | Save a synthetic standup reply for a registered member. Raw text is not logged. |
| `list_nonresponders` | Return today's nonresponders who have not already received a follow-up. |
| `generate_standup_digest` | Build today's AI-assisted digest string. **Does not post to Slack.** |
| `reset_synthetic_standup` | Local-only test reset. Requires `confirm=true`. Preserves team members. |
---
## MCP resource and prompt
**Resource** — `standup://skills`
Read-only combined view of `skills/summarize_standup.md`,
`skills/detect_blockers.md`, and `skills/format_digest.md`. Lets an
agent understand *how* the bot reasons without hitting the filesystem
itself. Loaded from a path derived from `__file__`.
**Prompt** — `standup_manager`
A reusable prompt that instructs an agent to (1) inspect the team,
(2) create or inspect today's run, (3) identify nonresponders,
(4) generate the digest, (5) never invent updates, (6) work with
synthetic data only, and (7) require explicit confirmation before
resets.
---
## Synthetic-data and security safeguards
- **Sample IDs are prefixed `SAMPLE_`** (`SAMPLE_U01`, `SAMPLE_U02`,
…) so it is obvious these are not real workspace users.
- **No secrets in code.** All credentials come from `.env`, which is
gitignored. `.env.example` is the source of truth for variable
names.
- **Guarded destructive actions.** The Slack `/reset-test-standup`
command requires `ALLOW_TEST_RESET` to be truthy; the MCP
`reset_synthetic_standup` tool requires `confirm=true`.
- **Never logs raw standup text.** `record_reply` in the service and
the DM handler in Slack print short confirmation lines and DB
identifiers, never the message body.
- **Parameterized SQL** everywhere in `db.py`.
- **MCP tools do not send Slack messages.** The agent surface can
only manipulate local state and generate text.
- **CI uses dummy env values** (no `secrets.*` context) and tests
wipe env vars before every test.
- See `SECURITY.md` for the credential-rotation policy.
---
## Known limitations
- **Free-model quality varies.** `OPENROUTER_MODEL=openrouter/free`
is intentionally the default; some free slugs on OpenRouter don't
fully support function-calling. If `mcp_agent.py` reports empty
tool calls, swap in another free tool-capable slug in `.env`.
- **DMs from unregistered users are silently dropped.** The DM
listener uses `save_response`, which only updates an existing
pending row created by `/start-standup`. That's intentional but
worth knowing.
- **SQLite is single-process.** The bot and the MCP server can both
read/write the same DB fine, but this is not a multi-node design.
- **Timezone / date drift.** `db.get_or_create_today_run` uses the
system's local date, while the scheduler runs on
`SCHEDULE_TIMEZONE`. Run the bot in a machine timezone that matches
the scheduler tz, or set both to UTC.
- **No auth on the MCP endpoint.** `mcp_server.py` is intended for
local `localhost:8000` use only. Do not expose it to the public
internet.
- **Model summaries are LLM output.** Occasional hallucinations are
possible; the safety prompt and fallback path minimize this, but a
human still owns the standup.
---
## Future improvements
- Retry policy + exponential backoff around OpenRouter calls.
- Optional weekly rollup posted on Fridays.
- Optional blocker-only "escalation" DM to a designated channel.
- Structured metrics (Prometheus) for run health.
- Multi-team / multi-channel support driven by DB config rather than
env vars.
- Auth (bearer token or OAuth) in front of the MCP endpoint before
exposing it beyond `localhost`.
- End-to-end integration tests against a mocked Slack API.
---
## Demo script
A ~5-minute walkthrough you can use verbatim:
1. **Show the architecture.** Open the Mermaid diagram above. Explain
that Slack, the scheduler, and MCP all share `standup_service.py`
so business logic is defined once.
2. **Show `.env.example` and `.gitignore`.** Emphasize: real
secrets live in `.env`, which is gitignored; the repo itself has
no tokens.
3. **Boot the Slack bot.**
```bash
python3 app.py
```
Point out the `⚡ Daily Standup Digest Bot is connecting to Slack...`
line and (with `ENABLE_SCHEDULER=true`) the printed cron schedule.
4. **In Slack:**
- `/join-standup` — add yourself.
- `/start-standup` — the bot DMs you.
- Reply in DM with a short synthetic update.
- `/post-standup-digest` — bot posts the AI-summarized digest to
`TEAM_CHANNEL_ID`.
5. **Boot the MCP server** (Ctrl-C the Slack bot first if you're
sharing one terminal):
```bash
python3 mcp_server.py
```
6. **Show MCP Inspector.**
```bash
npx -y @modelcontextprotocol/inspector
```
Connect to `http://localhost:8000/mcp`, walk through the six tools,
the `standup://skills` resource, and the `standup_manager` prompt.
7. **Run the LLM-driven MCP agent** against the running server:
```bash
python3 mcp_agent.py "Who has not responded today?"
```
Point out that the agent discovered the tools dynamically, that
OpenRouter chose which to call, and that MCP tools never posted
to Slack.
8. **Run the tests.**
```bash
pytest -q
```
All 54 tests pass offline in under a second. Show the CI badge (or
the `.github/workflows/test.yml` file) to demonstrate the same
suite runs on GitHub Actions across Python 3.12 and 3.13.
---
## License
MIT — see [`LICENSE`](LICENSE).
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues