> For the complete documentation index, see [llms.txt](https://klassdocs.gitbook.io/klassapp-documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://klassdocs.gitbook.io/klassapp-documentation/docs/ops/slack-connector-go-live-checklist.md).

# Slack MCP Connector — Production Go-Live Checklist

> **Status**: prep doc, 2026-09-20. The connector is built and dormant (PR #684, wave-1). This doc turns it on **safely**. Nothing here has been executed against production. Credentials are deliberately **not** in this file — every secret lives in Doppler.
>
> Verified against `main` @ `89481aef` (routes/ai.php live-mode block, `config/toshi.php` mcp\_connectors + mcp\_write\_gates, `config/services.php` slack\_mcp, `IntegrationsController`, `SlackSkill`, `McpWriteGate`, `ApprovableMcpTool`, vendor `laravel/mcp` OAuthRouteRegistrar/WebClient, prod-style `--no-dev` install re-check).

***

## 0. CRITICAL ORDERING CONSTRAINT — read before anything else

**The MCP write gate is master-switch-gated, and its default is OFF — which means "no gate".**

`McpWriteGate::isWrite()` (`app/Services/Toshi/McpWriteGate.php:23`) short-circuits:

```php
if (! config('toshi.mcp_write_gates.master_switch', false)) {
    return false;   // ← EVERY tool (including slack_post_message) classifies as "read"
}
```

With `TOSHI_MCP_WRITE_GATES_ENABLED` unset/false, `ApprovableMcpTool::needsApproval()` returns `false` for `slack_post_message` → **posts execute immediately, no human approval, defense-in-depth `assertExecutable()` passes too**. This is exactly the "ungated MCP write regression" the connector plan (A.3/A.5) exists to prevent.

**Therefore the write gates MUST be on (Step 3, batch B) in the same deploy — and strictly BEFORE `TOSHI_SLACK_CHANNEL_ENABLED=true`.** Never ship the channel flag without the gate flag.

Second hazard: `TOSHI_SLACK_CHANNEL_ENABLED=true` **while** `SLACK_MCP_MODE=mock` (the default) leaves the named client pointing at the local mock fixture server — school admins would get fake Slack data from a real-looking UI. **Always set `SLACK_MCP_MODE=live` and `TOSHI_SLACK_CHANNEL_ENABLED=true` in the same batch, never one alone.**

***

## 1. Every env var that gates live mode (complete list, code-verified)

| #  | Env var                         | Default                                                                       | Where read                                                                   | What it is                                                                                                                                                                                                                                                                       |
| -- | ------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1  | `SLACK_MCP_MODE`                | `mock`                                                                        | `config/services.php:78` → `routes/ai.php:20`                                | Mode branch for the named `slack` MCP client. **Any value that is not exactly `mock` = live mode** (typo → accidental live). `live` selects the remote `mcp.slack.com/mcp` client + registers the per-school OAuth connect/callback routes.                                      |
| 2  | `SLACK_MCP_URL`                 | `https://mcp.slack.com/mcp`                                                   | `config/services.php:79` + `config/toshi.php` catalog `endpoint`             | Remote MCP endpoint override. Leave default unless Slack changes the URL.                                                                                                                                                                                                        |
| 3  | `SLACK_MCP_CLIENT_ID`           | unset                                                                         | `config/services.php:80` + `config/toshi.php` catalog `oauth_client_id`      | OAuth client ID of **our registered Slack app**. Dual consumer: OAuth flow (`routes/ai.php:42`) **and** the registry token-refresh service (`McpConnectorTokenRefreshService`).                                                                                                  |
| 4  | `SLACK_MCP_CLIENT_SECRET`       | unset                                                                         | `config/services.php:81` + catalog `oauth_secret`                            | OAuth client secret. Same dual consumer as #3.                                                                                                                                                                                                                                   |
| 5  | `TOSHI_SLACK_CHANNEL_ENABLED`   | `false`                                                                       | `config/toshi.php` catalog `enabled`                                         | Master gate for the **feature surface**: SlackSkill instructions/tools, and the Slack tile in School Settings → Integrations (`IntegrationsController` filters the catalog on it). False = connector invisible everywhere.                                                       |
| 6  | `TOSHI_MCP_WRITE_GATES_ENABLED` | `false`                                                                       | `config/toshi.php` `mcp_write_gates.master_switch` → `McpWriteGate::isWrite` | Master switch for write classification **across all connectors**. **Must be `true` for any live connector** (see §0).                                                                                                                                                            |
| 7  | `TOSHI_SLACK_MCP_WRITE_MODE`    | `deny`                                                                        | `config/toshi.php` `mcp_write_gates.connectors.slack.mode`                   | How Slack tools classify: `deny` = fail closed (ALL tools incl. reads pause for approval — nothing works without approvals); `classify` = catalog `read_tools` free, `write_tools` gated (**recommended**); `allowlist` = only `slack_post_message` gated, everything else free. |
| 8  | `SLACK_MCP_TIMEOUT`             | `30`                                                                          | `config/services.php:83` → `routes/ai.php` client timeout (s)                | HTTP timeout for MCP calls. Leave default.                                                                                                                                                                                                                                       |
| 9a | `SLACK_MCP_SCOPES`              | `channels:read channels:history groups:history search:read.public chat:write` | `config/services.php` `slack_mcp.scopes` → `routes/ai.php` withOAuth         | OAuth scope string for the authorize step. Must be real Slack granular scopes from `mcp.slack.com`'s published metadata — see §4 scope table. Change requires a redeploy only, not a code change.                                                                                |
| 9b | `SLACK_MCP_TOKEN`               | unset                                                                         | `config/services.php:82`                                                     | ⚠️ **Dead config** — no runtime consumer exists in `app/`/`routes/` (verified by grep). Spike-era leftover. **Do not set in production**; a future cleanup PR should remove it.                                                                                                  |

Prerequisites already outside this feature's own flags (Slack rides on the Toshi v2 stack — these are presumably already live; verify, don't assume):

| Env var                                                                           | Why it matters here                                                                                                                                                                                                                                                      |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `TOSHI_SDK_V2_ENABLED`                                                            | `AgentToshi`/`ToshiSdkV2Service` route to `ToshiOrchestrator`, which registers `RouteToSlackSkillTool`. Off = Slack tool unreachable no matter what Slack flags say.                                                                                                     |
| `OPENAI_COMPATIBLE_URL` / `OPENAI_COMPATIBLE_MODEL` / `OPENAI_COMPATIBLE_API_KEY` | `SlackSkill` uses `UsesToshiLlm` — it calls `prompt()` itself and resolves the same provider/model. Misconfigured LLM = skill cannot run (and the known `AmbiguousToshiLlmConfigException` guard will fire loudly if the two env families conflict).                     |
| `APP_URL`                                                                         | The OAuth redirect URI is built with `route('mcp.oauth.slack.callback')` (vendor `WebClient.php:73`) — it resolves against APP\_URL. On Cloud this must be `https://klassapp.xyz` (staging: the `*.laravel.cloud` URL). Wrong APP\_URL = redirect-URI mismatch at Slack. |
| `schools.toshi_enabled` (DB, per school)                                          | Per-school Toshi gate (`config/toshi.php` `per_school_gate`). A school without it has no Toshi at all — no Slack either.                                                                                                                                                 |

## 2. What each flag controls (wiring map, verified)

* `routes/ai.php`: `mode=mock` → stdio mock client; `else` (anything ≠ 'mock') → `Client::web(mcp.slack.com/mcp).withOAuth(client_id, client_secret, config('services.slack_mcp.scopes'))` with per-request token closure → `SchoolMcpConnector::resolveTokenForRequest('slack')` → throws `ConnectorNotConnected` when the school has no active registry row. Live mode also registers `mcp/slack/connect` + `mcp/oauth/slack/callback` (vendor `OAuthRouteRegistrar`, `web` middleware).
* OAuth callback handler (`routes/ai.php`): resolves `Auth::user()->school_id`, maps Slack `team_id` → registry row `external_team_id`, stores encrypted TokenSet, `status=active`, `write_mode=deny` (row-level value is display-only — see §4).
* `IntegrationsController@index`: shows the Slack tile only when catalog `enabled`; Connect button only when `SLACK_MCP_MODE==='live' && filled(SLACK_MCP_CLIENT_ID)` (`slackConnectable`). Route group is `web,auth,fullschooladmin` (`RouteServiceProvider`, `admin/settings/integrations`).
* `SlackSkill`: `enabled=false` → no tools + "disabled" instructions; school not connected → catches `ConnectorNotConnected` and returns a friendly `slack_status` tool telling the admin to connect in Settings → Integrations. Reads execute immediately (audited); writes pause for approval via `ApprovableMcpTool` when the gate classifies them as writes.
* **Per-school write gating note**: the registry row's `write_mode` column (`deny` default) is **not consulted by the gate** — classification is env-level only (§1 #7). The row's `write_mode` is currently display-only in the UI. Per-school kill switch = the row's `status` (`active`/`disabled`), which *is* consulted by `resolveTokenForRequest()`.

## 3. Failure modes with wrong/placeholder values (misconfiguration risk table)

| Misconfiguration                                                                | Behavior                                                                                                                                                                                                                                                                                                                                                             | Loud or silent?                                                                                      |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `TOSHI_MCP_WRITE_GATES_ENABLED` off + Slack enabled                             | **Writes execute ungated.** Silent at config time; the ONLY trace is the audit row showing no approver.                                                                                                                                                                                                                                                              | **SILENT — worst case.** This is why §0 exists.                                                      |
| `TOSHI_SLACK_CHANNEL_ENABLED=true` + `SLACK_MCP_MODE=mock`                      | UI tile shows but says "Connect unavailable (instance not in live mode)"; Toshi's Slack answers come from the **mock fixture server**.                                                                                                                                                                                                                               | Semi-silent — tile visible, but connect impossible; data is fake.                                    |
| `SLACK_MCP_MODE` typo (e.g. `Live`, `production`)                               | Anything ≠ `mock` = live mode. With credentials absent: `withOAuth(null, null)` → OAuth falls back to RFC 7591 dynamic client registration against Slack's auth server. Slack does not support it → the connect route errors. Connect button is already hidden (`filled(client_id)` guard) but the route itself is registered and would fail loudly if hit directly. | Loud (500 at the route), UI guarded.                                                                 |
| Client ID/secret placeholder or wrong                                           | OAuth discover → authorize redirect renders; Slack rejects the client → error page on Slack's side. Secret wrong → token exchange fails at callback with `OAuthException`.                                                                                                                                                                                           | Loud (user-visible error at OAuth).                                                                  |
| School not connected (no registry row)                                          | `resolveTokenForRequest` → null → `ConnectorNotConnected` → skill returns friendly `slack_status` message: "connect it in School Settings → Integrations".                                                                                                                                                                                                           | Fail-closed, friendly, **by design**.                                                                |
| Token expired + refresh fails (e.g. secret rotated in Doppler but not deployed) | Refresh posts to `slack.com/api/oauth.v2.user.access` (user-token endpoint — the MCP flow issues user tokens, not bot tokens), fails, logs error, `resolveTokenForRequest` returns null → same friendly "not connected" state. Token stays active in DB — next successful refresh recovers.                                                                          | Semi-silent (Laravel log has `McpConnectorTokenRefresh: refresh failed`; user sees "not connected"). |
| `APP_URL` wrong/missing                                                         | `route('mcp.oauth.slack.callback')` resolves to wrong host → Slack rejects redirect\_uri mismatch at authorization.                                                                                                                                                                                                                                                  | Loud at first connect attempt.                                                                       |

## 4. Slack-side app configuration (what the human creates in api.slack.com)

* Create a Slack app (api.slack.com/apps → "From scratch" or per Slack's hosted-MCP instructions), one per KlassApp (single global OAuth app; per-school rows are data, not apps — one app, many `school_mcp_connectors` rows).
* **Redirect URI must be registered exactly** (code builds it via `route('mcp.oauth.slack.callback')`):

  * Production: `https://klassapp.xyz/mcp/oauth/slack/callback`
  * Staging: the staging host from the private repo KlassApp-Foundation/internal or the Cloud dashboard, path `/mcp/oauth/slack/callback`

  (Route = `mcp/oauth/{client}/callback`, client `slack` — vendor default, no custom URI passed in `routes/ai.php`. Trailing slashes and path variants will fail — OAuth redirect\_uri matching is byte-exact.)
* **Scope table — every scope KlassApp requests, verified against Slack's live MCP metadata** (`mcp.slack.com/.well-known/oauth-authorization-server`, fetched 2026-09-20 — 30 granular scopes published; `mcp:read`/`mcp:write`/`mcp:use` are absent from that list and belong to other vendors' MCP servers, not Slack's):

  | KlassApp scope (default `SLACK_MCP_SCOPES`) | Needed by                                      |
  | ------------------------------------------- | ---------------------------------------------- |
  | `channels:read`                             | `slack_list_channels`                          |
  | `channels:history`                          | `slack_get_channel_history` (public channels)  |
  | `groups:history`                            | `slack_get_channel_history` (private channels) |
  | `search:read.public`                        | `slack_search`                                 |
  | `chat:write`                                | `slack_post_message`                           |

  The scope string is env-configurable (`SLACK_MCP_SCOPES`, default `channels:read channels:history groups:history search:read.public chat:write`, commit `428f0d0e`) so a future scope change needs only an env-var update + redeploy, not a code change. To request additional tools later (e.g. user lookup → `users:read`), add the scope in Slack's app config **first**, then extend `SLACK_MCP_SCOPES`. The old hardcoded `mcp:read mcp:write` was never a valid Slack scope pair and caused "Invalid permissions requested" at authorize (see knowledge.md 2026-09-20 diagnosis).
* **MCP feature toggle REQUIRED**: the app must have **Features → Agents & AI Apps → Model Context Protocol toggled ON**. Without this toggle the app cannot use Slack's hosted MCP server at all.
* **Scopes go on User Token Scopes, not (only) Bot Token Scopes.** Slack's MCP OAuth flow is **user-token based**: authorize endpoint `slack.com/oauth/v2_user/authorize`, token endpoint `slack.com/api/oauth.v2.user.access` (per Slack's live metadata). The catalog `token_url` in `config/toshi.php` was corrected to the user-token endpoint accordingly (was the bot-token `oauth.v2.access` — a latent refresh bug). Keeping a minimal Bot Token Scopes set is fine (Slack's own MCP sample manifest uses both user
  * bot scopes), but the five MCP scopes above **must** be on User Token Scopes or the authorize step fails.
* **The app must be installed to the target workspace as an internal app** (or Marketplace-published — unlisted apps cannot use Slack's MCP server at all). Install to the pilot workspace **after** the MCP toggle + User Token Scopes are set, so the install grants the new scope set.
* **Token refresh contract** (audited 2026-09-20): `McpConnectorTokenRefreshService` posts `grant_type=refresh_token` to `https://slack.com/api/oauth.v2.user.access` and reads `access_token` / `refresh_token` / `token_type` / `expires_in` — standard OAuth field names, identical in Slack's user-token and bot-token responses, so the existing parsing is compatible; no bot-token-only field assumptions exist in the service.
* Note the app's Client ID and Client Secret → Doppler (§5), never the repo.
* Transport-era caveat (accepted, tripwired): our client speaks the pre-2026 MCP era (laravel/mcp 0.8.x); `mcp.slack.com` serves it until at least the 2027-04-28 re-verification date (`tests/Architecture/McpTransportEraReverificationTest.php`). No action for go-live.

## 5. Where secrets live

Doppler project **`klassapp`** (configs `dev` / `stg` / `prd` — existing convention, see knowledge.md). Keys to set in `stg` first, then `prd`:

* `SLACK_MCP_CLIENT_ID`
* `SLACK_MCP_CLIENT_SECRET`

Then mirror into Laravel Cloud environment variables (Cloud env vars only take effect after a **real deployment** — knowledge.md rule). The other flags (`SLACK_MCP_MODE`, `TOSHI_SLACK_CHANNEL_ENABLED`, `TOSHI_MCP_WRITE_GATES_ENABLED`, `TOSHI_SLACK_MCP_WRITE_MODE`) are non-secret and can go straight into Cloud env vars.

Never commit secret values to the repo, knowledge.md, or PR text.

## 6. Recommended rollout order

Per-school gating **is supported by design**: the connector is per-`school_mcp_connectors` row (school\_id + team\_id). The env flags are instance-wide, but a school only gets Slack functionality when **its own admin completes the OAuth connect** — schools without a row get the friendly "not connected" status. So the pilot mechanism = "only the pilot school connects".

1. **Batch A (wire, still dark):** Doppler `stg`: `SLACK_MCP_CLIENT_ID/SECRET`; Cloud staging env: `SLACK_MCP_MODE=live`, `TOSHI_MCP_WRITE_GATES_ENABLED=true`, `TOSHI_SLACK_MCP_WRITE_MODE=classify`. Deploy staging. Verify: integrations tile shows, connect works with the staging admin, mock→live switch has no stray mock server.
2. **Staging end-to-end:** connect the staging school's workspace, run reads (list/search/history) + a gated `slack_post_message` → approve in the panel → message lands in the real channel; verify audit rows + disconnect/reconnect.
3. **Batch B (production, pilot dark):** Cloud prod env: same flags as staging (`SLACK_MCP_MODE=live`, `TOSHI_MCP_WRITE_GATES_ENABLED=true`, `TOSHI_SLACK_MCP_WRITE_MODE=classify`) but **leave `TOSHI_SLACK_CHANNEL_ENABLED=false`** for the first deploy so nothing user-visible changes. Verify OAuth routes exist (`/mcp/slack/connect` 302s to Slack, not 500) and logs are clean.
4. **Batch C (pilot on):** set `TOSHI_SLACK_CHANNEL_ENABLED=true`, deploy, have **only the pilot school's** fullschooladmin connect. Verify end-to-end with them: reads + one approved write. Confirm other schools still show "not connected" (per-school isolation).
5. **Broaden:** schools connect at their own pace; keep `classify` mode. Revisit `allowlist` vs `classify` only if read-volume friction appears (allowlist gates only `slack_post_message`, everything else free — slightly less conservative).
6. Only after stable: consider the deferred wave-2 items (inbound events, interactive approval blocks) — out of scope here.

## 6b. Verified: role scoping within a connected school (fold-in check, 2026-09-20)

One admin's OAuth grants workspace *reachability* for the whole school. Verified that reachability ≠ usability: **a role check exists today, on two layers**, and is proven by test (`tests/Feature/Toshi/SlackRoleScopingTest.php`, 5 tests / 10 assertions, green):

1. **Structural layer** — `ToshiSdkV2Service`'s scope router sends each usergroup to its own operations agent: ug4 (deputy) / ug5 (teacher) / ug6 (student) / ug7 (parent) / ug8 (librarian) / ug10 (receptionist) / ug11 (accountant) → agents whose `tools()` **do not include `RouteToSlackSkillTool`** (verified by grep across all of them). Only the `default` arm → `ToshiOrchestrator` registers the Slack route tool.
2. **In-tool Gate layer** — `RouteToSlackSkillTool::handle()` calls `AuthorizesToshiAction::authorizeOrMessage()` → Gate `toshi-school-action` (ug3 SchoolAdmin, or ug1 SiteAdmin **impersonating** ug3) OR `toshi-deputy-action` (ug4, school-scoped). Everyone else gets "❌ You are not authorized for this school action." — including the unlisted groups (ug9 OldStudent, ug12 StockKeeper, ug1 in plain school scope) that fall through the structural layer's `default` arm; the in-tool Gate is the barrier for those.

**Who can invoke Slack at a connected school today:** SchoolAdmin (ug3), SiteAdmin impersonating a SchoolAdmin, and Deputy Admin (ug4) — dual-allowed by design (same tool surface ug3 and ug4 share, per the trait's docblock). **Teachers cannot** — proven for both a read intent and a write-gated post-message intent at a school with an ACTIVE registry row (denial comes from the role gate, not the not-connected fallback; zero audit rows produced). Writes additionally remain approval-gated regardless of role.

**Go/no-go impact:** **No blocker.** Role scoping is present, layered, and regression- pinned; this is a green verification, not a gap. Proceed with Batch A/B as written. (The deputy-admin dual-allow is the one deliberate breadth decision to keep in mind — if the pilot school wants Slack restricted to the full-school-admin only, that is a product choice to revisit, not a defect.)

## 6c. STATUS (updated 2026-10-02): connect flow VERIFIED; agent-loop E2E **re-run 2026-10-02 after #955 still FAIL**; wave-1 **not** done; Classroom [#728](https://github.com/KlassApp-Foundation/KlassApp/pull/728) still blocked (see knowledge Current Status)

**Outcome: FAIL.** Live Gemini agent-loop pass on staging. Evidence: `e2e/screenshots/slack-6c-gemini-e2e/REPORT.json` (local; may not be committed). Production untouched; no app code patched this pass; Classroom #728 still blocked. Slack wave-1 is **not** done.

**Re-run after #955:** [#955](https://github.com/KlassApp-Foundation/KlassApp/pull/955) MERGED `c1baafed` + staging deploy succeeded; §6c re-run still **FAIL** (`REPORT.json` run3 — blocked at `no_write_tool_pause`; keyword/chip path, #955 fixes not clearly exercised). Wave-1 not done; #728 still blocked.

**Observed (PARTIAL):** `RouteToSlackSkillTool` invoked; `mcp_tools_slack_search_channels` stuck `pending_approval`; no UI confirm card; write / approve / reject **not** exercised. Suspected catalog name mismatch: config `read_tools` has `slack_list_channels` / `slack_search` / `slack_get_channel_history` vs live tool `slack_search_channels`.

**Five checks — outcome FAIL:**

**What is verified working, end-to-end, against real Slack (not mocks):**

* Scope fix (`channels:read channels:history groups:history search:read.public chat:write`, env `SLACK_MCP_SCOPES`) — Slack's authorize accepts it (no "Invalid permissions requested").
* User-token endpoints throughout (authorize `oauth/v2_user/authorize`, refresh `oauth.v2.user.access`).
* The Slack app's MCP configuration (feature toggle, User Token Scopes, internal install).
* **A real workspace OAuth consent completed** — the callback upserted a real `school_mcp_connectors` row on staging (school\_id 2, `status=active`, `auth_mode=oauth_remote`, user-token credentials stored encrypted). The connection mechanism is proven: connect → consent → callback → registry row → token resolvable.

**LLM gate (historical — CLOSED as of 2026-10-02):**

* Earlier deferral (2026-09-20) said E2E tool-use was blocked because staging had no working LLM (`OPENAI_COMPATIBLE_URL`/`MODEL` NULL). That framing is **obsolete**.
* Staging Toshi LLM is **FIXED**: Gemini via `openai-compatible` (`OPENAI_COMPATIBLE_MODEL=gemini-3.8-flash`, host `generativelanguage.googleapis.com`); `toshi:llm-health` **OK** after deploy . Production LLM untouched.
* §6c was simply **never re-entered** after the LLM fix landed — not still waiting on an LLM gap.

**Five checks (listed — outcome FAIL on 2026-10-02 re-run):**

1. **Read tool E2E** — `slack_list_channels` / `slack_search` / `slack_get_channel_history` through a **live Toshi conversation** (Orchestrator → `RouteToSlackSkillTool` → `SlackSkill`), not a direct code bypass of the agent loop.
2. **Write-gate pause** — a conversational ask that should call `slack_post_message` must **PAUSE** for approval via `ApprovableMcpTool` (`TOSHI_SLACK_MCP_WRITE_MODE=classify`, master switch on — already set on staging).
3. **Approve path** — approve in the Toshi panel → message lands in the real channel; audit row records the approver.
4. **Reject path** — reject → no message; audit row records rejection.
5. **Real-vs-mock shapes** — compare real tool response shapes against `SpikeSlackMockServer` fixtures for the skill's parsing.

**Gemini-specific check (provider is OpenAI-compatible shim, not native OpenAI):** confirm laravel/ai's tool-calling loop correctly recognizes Gemini tool calls — SlackSkill fires, write pauses on Approvable, resume after approval works. Differences vs earlier direct-invocation tests are significant findings, not flakes.

**What this state is and is NOT:**

* It IS: **connection mechanism proven** + **LLM still OK** + §6c **ran and FAILED** on Gemini (PARTIAL route; stuck pending\_approval / no confirm UI).
* It is NOT: **"Slack wave-1 is done."** Re-run after catalog/UI-confirm follow-up. That gap **must** close before any pilot school goes live with real usage.
* Classroom PR [#728](https://github.com/KlassApp-Foundation/KlassApp/pull/728) stays **blocked from merge** until this §6c pass succeeds (hard sequencing).

**Staging fixture note:** `school_id=2` temporarily activated + Toshi assistant; synthetic admin `phase4.slack-e2e@klassapp.xyz` (id 553) created for E2E — **deactivate after follow-up**.

**Connection state decision (2026-09-20, still stands):** demo-school connector row LEFT ACTIVE (option a). Disconnect later only via Integrations UI if needed — never a hand-edited DB write.

## 7. Rollback plan

* **Instance-wide kill switch:** set `TOSHI_SLACK_CHANNEL_ENABLED=false` → connector invisible, skill tools empty, RouteTo tool answers "disabled". One env var + deploy.
* **Full revert to dormant state:** also set `SLACK_MCP_MODE=mock` → named client returns to the local mock server; OAuth routes unregister. (Leave `SLACK_MCP_MODE=live` + `TOSHI_SLACK_CHANNEL_ENABLED=false` if you want to keep the wiring warm instead.)
* **Per-school emergency off:** School Settings → Integrations → Disconnect → registry row `status=disabled` (never deleted — standing rule #3). Takes effect on the next token resolution; no deploy needed.
* **Data:** rollback is env-var + registry-status only. The `school_mcp_connectors` table and its rows persist harmlessly; encrypted tokens just sit unused. **No data migration is needed to undo any of this** (the feature shipped fully in #684; nothing to un-migrate).
* **Audit note:** every read/write already lands in the Toshi MCP audit trail regardless of rollback — post-incident review works even with the connector off.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://klassdocs.gitbook.io/klassapp-documentation/docs/ops/slack-connector-go-live-checklist.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
