Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
bdb8973
feat(access-control): let orgs restrict MCP access to read-only
a-lider Aug 20, 2026
d2acaa5
feat(access-control): keep the whole ceiling decision in the facade
a-lider Aug 20, 2026
7fabd47
feat(access-control): enforce channel ceilings as their own permissio…
a-lider Aug 20, 2026
0e7177d
feat(access-control): rename the model family to surface access limits
a-lider Aug 21, 2026
8559e39
chore(access-control): rewrite docstrings in simplified technical eng…
a-lider Aug 21, 2026
72f1f05
chore(access-control): say why SurfaceAccessLimit skips CreatedMetaFi…
a-lider Aug 21, 2026
24129cb
feat(access-control): make the all-resources wildcard explicit
a-lider Aug 21, 2026
321e7c4
chore(access-control): finish the simplified-english pass on the faca…
a-lider Aug 21, 2026
0d14d46
chore(access-control): rename the permission class to SurfaceAccessLi…
a-lider Aug 21, 2026
bbbb6a8
chore(access-control): register SurfaceAccessLimit in the org-scoped …
a-lider Aug 21, 2026
1f3922b
fix(access-control): drop a stale resource=None from the wildcard test
a-lider Aug 21, 2026
59a7de1
fix(access-control): a none limit denies reads too, and drop Any from…
a-lider Aug 21, 2026
fc2d5ab
refactor(access-control): store the MCP read-only flag as an organiza…
a-lider Aug 25, 2026
d9a83b9
fix(access-control): evaluate the MCP cap after membership permissions
a-lider Aug 25, 2026
3c3a2a0
Merge remote-tracking branch 'origin/master' into alex/feat/access-ce…
a-lider Aug 25, 2026
a525196
chore(access-control): track the renumbered hot-table migration ackno…
a-lider Aug 25, 2026
f5e4460
fix(access-control): satisfy the facade import contract and the test …
a-lider Aug 25, 2026
dd14a1d
fix(access-control): apply the target org MCP policy to connection fo…
a-lider Aug 25, 2026
d003640
test(backend): update query snapshots
tests-posthog[bot] Aug 25, 2026
3d2c581
chore(access-control): simplified-english pass on the refactored comm…
a-lider Aug 25, 2026
6a95042
fix(access-control): cap unclassified actions by method and restore t…
a-lider Aug 25, 2026
99ff58e
fix(access-control): declare the protocol headers member as a read-on…
a-lider Aug 25, 2026
a6e0dc0
refactor(access-control): split the MCP cap into classifier, policy a…
a-lider Aug 25, 2026
dd42bef
chore(access-control): simplified-english pass on the moved comments …
a-lider Aug 25, 2026
d9b4020
fix(access-control): cap MCP writes against the URL org, not the curr…
a-lider Aug 25, 2026
48de8c6
Merge remote-tracking branch 'origin/master' into alex/feat/access-ce…
a-lider Aug 25, 2026
6d8f3ef
refactor(access-control): always mark connection forwards as MCP
a-lider Aug 25, 2026
b499f9f
fix(access-control): stop over-denying connection forwards and cap ro…
a-lider Aug 25, 2026
bd2dd0d
test(access-control): make the enforcement tests prove the cap, not m…
a-lider Aug 25, 2026
931dc92
fix(access-control): classify ID-JAG tokens as MCP so the cap applies
a-lider Aug 25, 2026
5b5278c
feat(access-control): add the MCP read-only toggle to org security se…
a-lider Aug 21, 2026
da6285a
refactor(access-control): rename the flag to read_only_mcp_access
a-lider Aug 26, 2026
7a18fa2
Merge remote-tracking branch 'origin/master' into alex/feat/access-ce…
a-lider Aug 26, 2026
aa42fc3
Merge branch 'alex/feat/access-ceilings' into alex/feat/mcp-access-se…
a-lider Aug 26, 2026
5accd51
refactor(access-control): rename the flag to read_only_mcp_access in …
a-lider Aug 26, 2026
532819e
fix(access-control): allow read_only_mcp_access in the organization u…
a-lider Aug 26, 2026
91ab38a
test(backend): update query snapshots
tests-posthog[bot] Aug 26, 2026
a16ab0d
fix(access-control): clarify the MCP read-only setting copy
a-lider Aug 26, 2026
17b1edd
fix(access-control): say PostHog MCP without server in the setting copy
a-lider Aug 26, 2026
688c2ed
chore(access-control): delete a leftover half-line comment
a-lider Aug 26, 2026
15fd27f
chore(access-control): delete a leftover half-line comment
a-lider Aug 26, 2026
a60c2ab
fix(access-control): rename the settings section to MCP access
a-lider Aug 26, 2026
f705005
test(backend): update query snapshots
tests-posthog[bot] Aug 26, 2026
3ce6b84
Merge remote-tracking branch 'origin/master' into alex/feat/access-ce…
a-lider Aug 27, 2026
2b61964
Merge branch 'alex/feat/access-ceilings' of github.com:PostHog/postho…
a-lider Aug 27, 2026
c012777
Merge branch 'alex/feat/access-ceilings' into alex/feat/mcp-access-se…
a-lider Aug 27, 2026
c04fcde
chore(access-control): refresh stale kea-typegen blocks
a-lider Aug 27, 2026
ced9af2
chore(access-control): match the recording-viewed typegen block to th…
a-lider Aug 27, 2026
7c030ca
chore(access-control): sort the typegen union key alphabetically
a-lider Aug 27, 2026
d38047f
Merge remote-tracking branch 'origin/master' into alex/feat/mcp-acces…
a-lider Aug 27, 2026
99d33d4
Merging d38047fd67a2f9fcfcd9e131871cae16fb7394bd into trunk-temp/pr-8…
trunk-io[bot] Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions frontend/src/generated/core/api.schemas.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/src/scenes/organizationLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type OrganizationUpdatePayload = Partial<
| 'members_can_create_projects'
| 'members_can_use_personal_api_keys'
| 'members_can_see_org_members'
| 'read_only_mcp_access'
| 'is_ai_data_processing_approved'
| 'is_ai_training_opted_in'
| 'default_experiment_stats_method'
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/scenes/settings/SettingsMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { PreAggregatedTablesSetting } from 'scenes/settings/environment/PreAggre
import { ReplayTriggers } from 'scenes/settings/environment/ReplayTriggers'
import { SessionsTableVersion } from 'scenes/settings/environment/SessionsTableVersion'
import { SessionsV2JoinModeSettings } from 'scenes/settings/environment/SessionsV2JoinModeSettings'
import { OrganizationMCPAccess } from 'scenes/settings/organization/OrganizationMCPAccess'
import { urls } from 'scenes/urls'

import {
Expand Down Expand Up @@ -1908,6 +1909,13 @@ export const SETTINGS_MAP: SettingSection[] = [
component: <OrganizationSecuritySettings />,
keywords: ['compliance', 'sharing', 'public'],
},
{
id: 'organization-mcp-access',
title: 'MCP access',
description: 'Control what the PostHog MCP can do in this organization.',
component: <OrganizationMCPAccess />,
keywords: ['mcp', 'ai', 'agent', 'read-only', 'model context protocol'],
},
{
id: 'organization-personal-api-keys',
title: 'Personal API key access',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useActions, useValues } from 'kea'

import { IconInfo } from '@posthog/icons'
import { LemonSwitch } from '@posthog/lemon-ui'

import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini'
import { useRestrictedArea } from 'lib/components/RestrictedArea'
import { OrganizationMembershipLevel } from 'lib/constants'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { organizationLogic } from 'scenes/organizationLogic'

import { AvailableFeature } from '~/types'

export function OrganizationMCPAccess(): JSX.Element {
const { currentOrganization, currentOrganizationLoading } = useValues(organizationLogic)
const { updateOrganization } = useActions(organizationLogic)

const adminRestrictionReason = useRestrictedArea({
minimumAccessLevel: OrganizationMembershipLevel.Admin,
})

return (
<PayGateMini feature={AvailableFeature.ORGANIZATION_SECURITY_SETTINGS}>
<LemonSwitch
label={
<span>
Restrict MCP access to read-only{' '}
<Tooltip title="When enabled, anyone connecting to this organization through the PostHog MCP can read data but can't change it. This applies to every member, including admins. Each member's permissions still apply separately via access control. Using PostHog in the app or calling the API directly is not affected.">
<IconInfo className="mr-1" />
</Tooltip>
</span>
}
bordered
data-attr="org-mcp-access-read-only-toggle"
checked={!!currentOrganization?.read_only_mcp_access}
onChange={(read_only_mcp_access) => {
updateOrganization({ read_only_mcp_access })
}}
disabled={currentOrganizationLoading}
disabledReason={adminRestrictionReason}
/>
</PayGateMini>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface verifiedDomainImpactLogicActions {
| 'members_can_see_org_members'
| 'members_can_use_personal_api_keys'
| 'name'
| 'read_only_mcp_access'
>
>
) => Partial<
Expand All @@ -72,6 +73,7 @@ export interface verifiedDomainImpactLogicActions {
| 'members_can_see_org_members'
| 'members_can_use_personal_api_keys'
| 'name'
| 'read_only_mcp_access'
>
> // organizationLogic
closeEnforcementPrompt: () => {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/scenes/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export type SettingId =
| 'organization-proxy'
| 'organization-roles'
| 'organization-security'
| 'organization-mcp-access'
| 'organization-personal-api-keys'
| 'passkeys'
| 'login-sessions'
Expand Down
1 change: 1 addition & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ export interface OrganizationType extends OrganizationBasicType {
members_can_create_projects?: boolean
members_can_use_personal_api_keys: boolean
members_can_see_org_members?: boolean
read_only_mcp_access?: boolean
allow_publicly_shared_resources: boolean
metadata?: OrganizationMetadata
member_count: number
Expand Down
10 changes: 10 additions & 0 deletions posthog/api/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ class Meta:
"members_can_use_personal_api_keys",
"members_can_see_org_members",
"allow_publicly_shared_resources",
"read_only_mcp_access",
"member_count",
"is_ai_data_processing_approved",
"is_ai_training_opted_in",
Expand Down Expand Up @@ -397,6 +398,15 @@ def validate_members_can_see_org_members(self, value: bool) -> bool:
def get_member_count(self, organization: Organization) -> int:
return _cached_per_org("member_count", str(organization.id), lambda: _fetch_member_count(organization))

def validate_read_only_mcp_access(self, value: bool) -> bool:
if self.instance and self.instance.read_only_mcp_access != value:
if not self.instance.is_feature_available(AvailableFeature.ORGANIZATION_SECURITY_SETTINGS):
raise serializers.ValidationError(
"You must upgrade your plan to configure MCP access.",
code="payment_required",
)
return value

@tracer.start_as_current_span("organization_serializer.to_representation")
def to_representation(self, instance):
return super().to_representation(instance)
Expand Down
31 changes: 31 additions & 0 deletions products/access_control/backend/tests/test_mcp_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,37 @@
from posthog.models.utils import generate_random_token_personal, hash_key_value


class TestMCPAccessSetting(APIBaseTest):
def setUp(self) -> None:
super().setUp()
self.organization.available_product_features = [
{
"key": AvailableFeature.ORGANIZATION_SECURITY_SETTINGS,
"name": AvailableFeature.ORGANIZATION_SECURITY_SETTINGS,
}
]
self.organization.save()
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()

def test_toggle_round_trip(self) -> None:
response = self.client.patch("/api/organizations/@current/", {"read_only_mcp_access": True})
assert response.status_code == 200
assert response.json()["read_only_mcp_access"] is True
self.organization.refresh_from_db()
assert self.organization.read_only_mcp_access is True

response = self.client.patch("/api/organizations/@current/", {"read_only_mcp_access": False})
assert response.json()["read_only_mcp_access"] is False

def test_toggle_requires_the_entitlement(self) -> None:
self.organization.available_product_features = []
self.organization.save()
response = self.client.patch("/api/organizations/@current/", {"read_only_mcp_access": True})
assert response.status_code == 400
assert response.json()["code"] == "payment_required"


class TestMCPReadOnlyEnforcement(APIBaseTest):
def setUp(self) -> None:
super().setUp()
Expand Down
10 changes: 10 additions & 0 deletions products/platform_features/frontend/generated/api.schemas.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions products/platform_features/frontend/generated/api.zod.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions services/mcp/src/api/generated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions services/mcp/src/generated/platform_features/api.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions services/mcp/src/tools/generated/platform_features.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading