Slack /pr-* → MCP Migration¶
Owner: Brandon Shin Status: Planning — this doc only. No code in this PR.
This project retires the internal Slack app (/pr-* slash commands) in favor of the PaperRun MCP. The end state: every Slack command returns a static "use the MCP" message, the commands worth keeping have MCP-tool equivalents, and a later separate PR rips out the Slack app code entirely.
The work lands in three deliberately separated steps so each is small and reviewable:
- This doc — the migration map and rip-out plan.
- Stub PR — replace every
/pr-*handler body with a static "use the MCP instead" response; ensure the commands worth porting have MCP coverage. No code deletion. - Rip-out PR (later) — delete
slack_app_routes.py, the Slack methods, block formatters, signature verification, config, and tests.
1. Why¶
The Slack commands duplicate data access that the MCP already does better. The trigger was LTM revenue: /pr-org computes it, but so does the MCP — both call the same generate_organization_audit_slim() (app/methods/audits/organization_audit.py), which already returns ltm_revenue/ltm_orders. Maintaining two front-ends over one computation is the cost we're removing. The MCP also has fuzzy org search, campaign stats, and reporting payloads that the Slack app never grew.
2. Current Slack surface¶
Routing: POST /api/v1/slack → signature verify → SLASH_COMMANDS dict dispatch (app/routes/slack_app_routes.py). Interactive buttons: POST /api/v1/slack/interactive → handle_block_action.
| Command | Handler (app/methods/slack.py) |
What it returns |
|---|---|---|
/pr-hello |
handle_hello_command |
Connectivity greeting. |
/pr-orgs |
handle_orgs_command |
First 50 orgs as a table (id, name, url). |
/pr-org <org> |
handle_org_command |
Full org snapshot: basic info + audit (LTM revenue, company size, integrations, flow indicators) + CSM insights. Fuzzy-matches org name with confirm buttons. |
Interactive: org_suggest_yes / org_suggest_no (fuzzy-match confirmation) → handle_block_action.
The /pr-org snapshot is assembled from three formatter blocks, each tangling computation with Slack-block rendering:
format_organization_basic_info— org identity, Retool/HubSpot/Stripe deep links, CSM, campaign list.format_organization_audit→format_audit_summary— LTM revenue,get_company_size_tag,categorize_integrations, birthday/loyalty flags.format_organization_insights— strong/weak campaign performers + new-flow suggestions.
3. Command → MCP mapping¶
| Slack command | Port? | MCP equivalent | Status |
|---|---|---|---|
/pr-hello |
No | — (connectivity check has no data value) | Drop in stub PR. |
/pr-orgs |
Yes | list_organizations |
Already exists (app/mcp/server.py). |
/pr-org basic info |
Yes | get_organization + new get_organization_overview |
Overview tool not yet built — see §4. |
/pr-org audit |
Yes | get_organization_audit_slim |
Exists; LTM revenue already returned. Derived company_size / categorized_integrations to be folded in — see §4. |
/pr-org insights |
Yes | new get_organization_insights |
Not yet built — see §4. |
| Fuzzy org-name match + confirm buttons | No (replaced) | search_reporting_organizations (fuzzy name/store match) |
The MCP client resolves names; no interactive equivalent needed. |
4. MCP coverage gap before stubbing¶
Stubbing /pr-org is only safe once its data is reachable through the MCP. The audit/overview/insights port is specced separately (local design + plan under docs/superpowers/, not published). Summary of what that work adds, all in app/methods/audits/organization_audit.py with thin MCP tools mirroring get_organization_audit_slim:
- Derived audit fields — move
get_company_size_tag/categorize_integrations(today inslack.py) into the audit module; add defaultedcompany_size+categorized_integrationsfields toSlimAuditResponse.get_organization_audit_slimreturns them for free. build_organization_overview→get_organization_overview— org identity, Retool/HubSpot/Stripe links, CSM email, campaign list.build_organization_insights→get_organization_insights— strong/weak/new-flow insights as structured data.
Sequencing: land the overview/insights tools, then the stub PR can safely point users at them.
5. Stub approach (next PR, not this one)¶
Keep the routes and signature verification so Slack doesn't 404, but replace each handler body with one shared deprecation response:
handle_hello_command,handle_orgs_command,handle_org_command→ return a single block: "The/pr-*Slack commands are deprecated. Use the PaperRun MCP — e.g. ask for the org audit, overview, or org list there." (with the command→tool mapping inline).handle_block_action(interactive) → same deprecation response.- Leave
SLASH_COMMANDSwired so any command resolves to the stub; no new routes.
Nothing is deleted yet — this keeps the diff tiny and the rollback trivial.
6. Rip-out (later, separate PR)¶
Once the stub has shipped and a release has passed with no fallback need:
- Delete
app/routes/slack_app_routes.pyand unregister its blueprint inapp/main.py. - Delete
app/methods/slack.pyand its block-formatting helpers. - Remove Slack signature verification, rate-limit decorators, and Slack
config.SLACK_*entries that are now unused. - Delete
app/tests/methods/test_slack.pyand any Slack route tests. - Grep for residual
slackimports/usages and remove.
7. Open questions¶
- Do any non-engineering stakeholders (CSMs) still rely on
/pr-orgin Slack day-to-day? If so, the stub message should name the exact MCP client/workflow to switch to before the rip-out. - Should
list_organizationsgain a fuzzy-name argument to fully cover/pr-orgs+ the/pr-orgname resolution, or issearch_reporting_organizationssufficient?