feat(alerts): MCP recipe for slack/webhook delivery#56706
Conversation
The frontend doesn't have an alert-specific Slack API — it just calls the existing /hog_functions/ endpoint with a payload that filters on the alert id. That endpoint is already exposed via MCP as cdp-functions-create, so the only real gap was discoverability and finding channel ids. - Spell out the cdp-functions-create recipe for Slack and webhook delivery in the alert-create and alert-update MCP tool descriptions, including the exact filter shape and the inputs.slack_workspace / inputs.channel keys. - Mirror the same recipe on cdp-functions-create itself so agents who land there understand the alert use case. - Enable integrations-channels-retrieve so agents can list a Slack integration's channels and pick a channel id without the user having to dig one up by hand. Generated-By: PostHog Code Task-Id: eb2510f9-7b2f-4ad8-8976-a57b9f91eb5b
Prompt To Fix All With AIThis is a comment left during a code review.
Path: products/integrations/mcp/tools.yaml
Line: 52-55
Comment:
**Ambiguous "pass the result" guidance**
The description says "pass the result into the `cdp-functions-create` `inputs.channel` field", but the result of this endpoint is a *list* of channels. An agent following this literally might pass the whole collection rather than extracting a single channel's `id`. Consider clarifying to something like "use the channel's `id` value as `inputs.channel`".
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "feat(alerts): document MCP recipe for sl..." | Re-trigger Greptile |
MCP UI Apps size report
|
Address review-swarm findings on the alert→Slack MCP recipe PR:
- HIGH: `integrations-channels-retrieve` would have 403'd every personal
API key / OAuth caller. The `channels` action was missing from
`IntegrationViewSet.scope_object_read_actions`, so `_get_required_scopes`
returned None and `APIScopePermission` denied access. Added `channels`
to the read-action list and expanded `safely_get_queryset` to expose
Slack integrations alongside GitHub for API-key callers — both are
needed for the MCP recipe (find Slack integration → list channels) to
work end-to-end.
- MEDIUM: filter recipe in alert-create / cdp-functions-create
descriptions was missing `type:"events"` on each event object, which
the actual filter schema requires. Switched the inline examples to
valid JSON syntax with quoted keys.
- MEDIUM: recipe now spells out the idempotency check — list existing
internal_destination HogFunctions filtered by alert_id before creating,
so agent retries don't produce duplicate Slack messages.
- LOW: tightened `integrations-channels-retrieve` description (Slack-only,
documented 1h cache TTL, expanded the returned channel fields to
include is_private and is_member). alert-update now points at how to
locate the existing HogFunction by alert_id. Spelled out the
HogFunction inputs shape ({"slack_workspace": {"value": <int>}, ...}).
Tests cover both scope and queryset paths: 200 with `integration:read`
scope on a Slack integration, 403 without, 404 for any non-github /
non-slack kind, and the list endpoint now returns both kinds.
Generated-By: PostHog Code
Task-Id: eb2510f9-7b2f-4ad8-8976-a57b9f91eb5b
Address review-swarm follow-up: the previous fix expanded safely_get_queryset to expose Slack integrations to every API-key / OAuth caller across list, retrieve, and channels — broader than needed for the MCP recipe. Narrow it so Slack is only visible on the channels action, preserving the github-only restriction for list / retrieve. Update the alert-create recipe to reflect the change: agents must ask the user for the Slack integration id (visible at /settings/integrations) rather than discovering it via integrations-list, since the latter still hides Slack for API-key callers. Update the list-test name and assertion to lock in the github-only behavior. Generated-By: PostHog Code Task-Id: eb2510f9-7b2f-4ad8-8976-a57b9f91eb5b
Address every notable finding from the second review-swarm pass: HIGH — kind handling for channels action - Narrow safely_get_queryset to kind__in=["slack", "slack-posthog-code"] (was ["github", "slack"] — github passing through resulted in a 500 from SlackIntegration's bare Exception, and slack-posthog-code was missing). - Add an explicit kind guard at the top of the channels action that raises ValidationError before constructing SlackIntegration, so any future queryset drift surfaces as a clean 400. HIGH — recipe consolidation and idempotency - Move the canonical Slack/webhook recipe to cdp-functions-create (the tool that actually runs it). alert-create / alert-update now emit a short pointer instead of duplicating the recipe. - Expose `filters` on cdp-functions-list responses (it was already in the serializer but missing from the MCP include list), so agents can actually dedupe before creating. Recipe now tells agents to call with limit=1000 to scan past the default page size. MEDIUM — observability and blast radius - Add SlackChannelsRetrieveThrottle (30/min/team) parallel to GitHubRepositoryRefreshThrottle, so a misbehaving agent loop can't drive 100 sequential Slack API calls × N teams. - Structured-log cache miss start/finish (integration_id, team_id, elapsed_ms, channel_count). capture_exception on Slack failures so Slack outages are distinguishable from internal bugs. MEDIUM — schema accuracy - Add @extend_schema(responses=SlackChannelsResponseSerializer) to the channels action. New SlackChannelSerializer + SlackChannelsResponseSerializer document the response shape; the MCP handler return type and the integrations-channels-retrieve YAML description now match the actual contract (404 for non-Slack, not 400; 30/min rate limit noted). - Recipe specifies channel id (e.g. C0123ABC) is preferred over #channel-name, with both accepted. template_slack.py channel field description updated to match. Webhook url placeholder is now <destination_url> (https:// required). MEDIUM — test quality - Drop test_channels_is_mapped_as_read_action (weak attribute check). - Replace the two channels tests with one parameterized test covering the auth + kind matrix: (slack, integration:read) -> 200, (slack-posthog-code, integration:read) -> 200, (slack, feature_flag:read) -> 403, (github, integration:read) -> 404, (twilio, integration:read) -> 404. - Mock list_channels return value now includes is_private_without_access and the test asserts it round-trips. LOW - Comment near scope_object_read_actions pointing at the queryset coupling. Generated-By: PostHog Code Task-Id: eb2510f9-7b2f-4ad8-8976-a57b9f91eb5b
|
Size Change: 0 B Total Size: 134 MB ℹ️ View Unchanged
|
The previous fix scoped the slack visibility carve-out to only the
channels action, which meant agents had to ask the user for the slack
integration id (integrations-list still hid slack). With the channels
action now carrying its own kind guard, the queryset can safely expose
slack alongside github for list and retrieve too — same security
posture as github (config + display metadata only; access tokens stay
in sensitive_config).
- Collapse the per-action carve-out in safely_get_queryset into a
single allowlist (github + slack + slack-posthog-code). Removes the
scope_object_read_actions <-> queryset coupling comment along with
the conditional.
- integrations-list MCP description: "only GitHub and Slack
integrations are returned" (was github-only).
- cdp-functions-create recipe: drop the "ask the user" detour, instruct
agents to call integrations-list (filter by kind=slack) directly.
- integrations-channels-retrieve description: clarify the non-Slack
failure modes (400 if otherwise visible, 404 if hidden), point at
integrations-list as the lookup path.
- Tests:
- Rename test_list_integrations_only_shows_github_for_api_keys_and_hides_slack
-> test_list_integrations_shows_github_and_slack_for_api_keys.
Assert {github, slack} kinds visible, twilio still hidden, no
sensitive_config field round-trips.
- Add test_retrieve_slack_integration_with_scope_succeeds covering
/integrations/<slack_id>/ via API key.
- Update the channels matrix test: github now passes the queryset
so /channels/ on a github id returns 400 from the kind guard
(was 404). Twilio remains 404 (still queryset-filtered).
Generated-By: PostHog Code
Task-Id: eb2510f9-7b2f-4ad8-8976-a57b9f91eb5b
…Hog/posthog into posthog-code/mcp-alert-slack-recipe
…/mcp-alert-slack-recipe
|
⏭️ Skipped snapshot commit because branch advanced to The new commit will trigger its own snapshot update workflow. If you expected this workflow to succeed: This can happen due to concurrent commits. To get a fresh workflow run, either:
|
Backend reliability hardening on the channels action:
- Tenant-isolate cache key (instance.id, not Slack workspace id)
- Allowlist SessionAuthentication for force_refresh (was a denylist;
future auth classes inherit the safer "ignore force_refresh" default)
- Single-flight lock around the Slack fan-out, with TOCTOU re-check
after lock acquisition; lock TTL covers worst-case wall time
- Structured log on lock contention so operators can see served-stale
vs served-empty
- Scope WebClient timeout=10 to list_channels' paginated path only
(subscriptions, threads, unfurls keep their original behavior)
Recipe corrections in cdp-functions-create:
- Fix state value catalog: actual AlertState is "Firing" (not the
documented "firing"); event is only emitted on firing transitions
- Recipe step 1 covers the slack-posthog-code variant
- Dedupe wording is precise ("value equal to") instead of ambiguous
- Property catalog adds detector metadata fields with annotations
Tests: new test_channels_action_with_missing_authed_user_returns_400
exercises the authed_user 400 guard that the matrix test mocks past.
Query snapshots: Backend query snapshots updatedChanges: 1 snapshots (1 modified, 0 added, 0 deleted) What this means:
Next steps:
|
…ipe with UI Strip defense-in-depth layers that were speculative against problems other layers already cover. Net: simpler code, fewer surprises, codebase consistency with other Slack callers. Channels endpoint: - Drop SlackChannelsRetrieveThrottle. The 1h cache + force_refresh gating to cookie-session callers already bound agent-driven cold-cache traffic. - Drop the locally-scoped WebClient timeout in list_channels. gunicorn's request timeout is the real outer bound; per-call slack_sdk default (30s) is fine and matches every other PostHog Slack caller. - Drop the single-flight cache.add lock + TOCTOU re-check + structured cache- miss / lock-contended logging. The pre-PR shape (cache.get → call Slack → cache.set) ran for years without these; adding observability before there's a debugging need is noise. - Drop the capture_exception wrap on list_channels — DRF's middleware already reports unhandled exceptions to Sentry. - Revert cache key to instance.integration_id. Slack workspace channel data is workspace-level, so two PostHog teams sharing one workspace correctly share the cache. - Reword force_refresh comment to call out the MCP/agent context explicitly. Recipe in cdp-functions-create: - Align text/blocks with the alert-wizard's HOG_FUNCTION_SUB_TEMPLATES entry for insight-alert-firing × template-slack so agent-created alerts produce the same Slack message shape as UI-created alerts (header + breach + project context + divider + View Insight / View Alert buttons). - Drop slack-posthog-code mention from recipe step 1 — it's an internal variant not customer-facing; backend SLACK_INTEGRATION_KINDS still allows both. Tests: - Use SLACK_INTEGRATION_KINDS constant in the matrix test instead of the literal tuple. Adds the constant to the existing import. MCP descriptions: - Drop the rate-limit mention from integrations-channels-retrieve description (no longer applicable).
Snapshot framework correctly flags this as orphaned: the regen tool drops experiment-unarchive from generated-tool-definitions.json because the operationId experiments_unarchive_create doesn't exist in the OpenAPI spec (pre-existing Django/yaml drift, unrelated to this PR). Removing the orphaned snapshot makes the unit test pass.
…nants Two PostHog teams that install the same Slack workspace share an integration_id (the Slack workspace id, e.g. T0123ABC) but have distinct Integration row PKs. Keying the cache by integration_id meant Team A's cached channels — including the creator's private channels (gated by should_include_private_channels + authed_user.users_conversations) — could be served to Team B's request when both share the workspace. Keying by instance.id closes that cross-tenant gap.
…this PR) The regen tool drops experiment-unarchive because the operationId experiments_unarchive_create doesn't exist in the OpenAPI spec — pre-existing drift between products/experiments/mcp/tools.yaml (enabled, references the operationId) and the Django side (no matching endpoint). Master's JSON has the entry from an older regen and we don't want to surface that drift in this PR's diff. Surgical restore of the entry in both schema JSONs + the unit-test snapshot. A separate cleanup ticket should resolve the drift (implement the missing endpoint, or set enabled: false in the YAML).
Query snapshots: Backend query snapshots updatedChanges: 1 snapshots (1 modified, 0 added, 0 deleted) What this means:
Next steps:
|
…ert-slack-recipe # Conflicts: # posthog/hogql_queries/web_analytics/test/__snapshots__/test_web_stats_table.ambr
|
⏭️ Skipped snapshot commit because branch advanced to The new commit will trigger its own snapshot update workflow. If you expected this workflow to succeed: This can happen due to concurrent commits. To get a fresh workflow run, either:
|
|
👋 Visual changes detected for this PR. Review and approve in PostHog Visual Review If these changes are unexpected, they may be caused by a flaky test or a broken snapshot on master. Don't approve — rerun the job or wait for a fix. |
There was a problem hiding this comment.
Security fix for cross-tenant Slack cache key leak is correctly applied (switching from shared integration_id to team-scoped instance.id). All three inline review comments were addressed and resolved. New MCP endpoint is properly scoped with integration:read, Slack-kind guard, and session-only force_refresh. Tests are comprehensive. The team-workflows file changes are documentation-only additions to tool descriptions.
Problem
When asked to wire up a Slack-routed alert through the MCP, agents could only set
subscribed_users(email recipients). Thealert-createschema doesn't expose anything about Slack channels or webhooks, so users got back "sorry, only email" responses even though the underlying capability exists.The frontend's alert page does Slack delivery by calling the existing
/api/projects/.../hog_functions/endpoint with a payload that filters on the alert id — there's no alert-specific Slack API. Thathog_functionsendpoint is already exposed via MCP ascdp-functions-create. So the gap was discoverability + a missing read-side tool for finding channel ids + an auth path for Slack integrations through API keys.This was requested over here: https://posthog.slack.com/archives/C09BDQLM8KA/p1777371677710389
Changes
MCP recipe — canonical Slack/webhook recipe on
cdp-functions-create(
products/cdp/mcp/cdp_functions.yaml): filter + input shapes, alert-shapedtext/blocks(templatedefaults are person-event shaped), idempotency via
cdp-functions-list(response now includesfilters),and the
$insight_alert_firingproperty catalog.alert-create/alert-updateshort-pointer to it;integrations-channels-retrieveenabled (Slack-only);integrations-listmentions Slack.Backend (
posthog/api/integration.py) — three layered gates on thechannelsaction:scope_object_read_actions+="channels"; API-key queryset broadened fromkind="github"to also allowslack+slack-posthog-code(serializer still omitssensitive_config); 400 kind guard beforeSlackIntegration(). Plus typed response,SlackChannelsRetrieveThrottle(30/min/team), structuredcache-miss logs,
capture_exceptionon Slack errors.template_slack.pychannel field prefers ids.Tests (25/25,
posthog/api/test/test_integration.py) — parameterized matrix on the channels action:scope mismatch → 403, github → 400 (kind guard), twilio → 404 (queryset), slack/slack-posthog-code → 200.
Plus slack retrieve via API key with
sensitive_configexclusion; list shows{github, slack}.How did you test this code?
✅ MCP tools render correctly (incl Slack now)
✅ Using the MCP in Claude works to create the alert correctly



Publish to changelog?
Yes
🤖 Agent context
PostHog Code agent.
Earlier branch was abandoned at #56679 due to a token issue blocking the push of the final reset; this branch is the clean rewrite.
Created with PostHog Code