fix(mcp): keep switch-organization available when only a project is pinned#73023
fix(mcp): keep switch-organization available when only a project is pinned#73023posthog[bot] wants to merge 1 commit into
Conversation
…inned A pinned projectId excluded both switch-organization and switch-project, which made the documented cross-org navigation flow impossible: from an active project an agent is supposed to resolve an org via organizations-get, call switch-organization, then switch-project to reach a project in another organization. Nearly every MCP connection pins a project, so the switch tools were almost never exposed. Only an explicitly pinned organization is now treated as a hard lock (drop switch-organization, keep project switching). A pinned project excludes nothing. Project-scoped keys (scoped_teams) are unaffected — getToolsForFeatures already strips organization:* tools the backend would 403 for such tokens. Generated-By: PostHog Code Task-Id: 5d4d5022-89c9-4f80-930c-63af7f569714
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc1fe22d93
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } else if (organizationId) { | ||
| excludeTools.push('switch-organization') | ||
| } | ||
| const excludeTools = switchToolsToExclude({ organizationId }) |
There was a problem hiding this comment.
Keep project switches from being overwritten
When a connection sends project_id/x-posthog-project-id on every MCP request, this exposes switch-project even though the resolver still writes props.projectId back into the token cache at the start of each request (reqCtx.tokenCache.setMany above). In the documented flow, switch-project can set the new project during its call, but the next tool call resolves state again and immediately restores the originally pinned project, so subsequent project-scoped tools run against the old project. Either make the project pin a one-time/default value before advertising navigation, or keep hiding switch-project for truly hard-pinned project sessions.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed — this is a real gap, thanks for catching it.
switch-project writes projectId into the same token cache key that resolve() re-pins from props.projectId via setMany at the top of every request, and props.projectId is derived per-request from x-posthog-project-id / project_id (request-properties.ts:75, index.ts:229). So for a connection that keeps sending the project pin — the common case this PR is aimed at — a switch-project call is reverted on the very next request, and later project-scoped tools silently run against the originally pinned project. That's arguably worse than hiding the tool, since it now looks usable but no-ops across requests.
Worth noting switch-organization is not affected in the target scenario: the org is usually not pinned, so setMany never re-asserts orgId and the org switch persists. Only the project step gets clobbered — which means exposing switch-project on a project pin isn't sufficient on its own.
Fix direction: treat the project pin as a one-time default rather than a hard re-pin — only seed projectId from props when nothing is already cached (gate the setMany project write on the absence of a cached projectId), so a subsequent switch-project survives later requests. Will address before merge.
|
Thanks ReviewHog — noted, nothing to action from this pass. 🐼 |

Problem
Multi-org users can't reach a project in another organization over MCP. The catalog is supposed to support this:
switch-project's own description documents the recovery flow - if a project isn't found, "callorganizations-getandswitch-organizationfirst, then retryprojects-get."But the exposure logic contradicts that. When a request pins a
projectId, the resolver excluded bothswitch-organizationandswitch-project:Nearly every MCP connection pins a project, so the switch tools were almost never exposed, and the documented cross-org flow was impossible to execute from an active project.
For context on how this surfaced: a Signals scout searched a project-scoped catalog and found no org tools at all. That part is correct and expected - a project-scoped key (
scoped_teams) can't act across orgs, andgetToolsForFeaturesalready strips everyorganization:*tool the backend would 403 for such a token. So this change does not alter what a project-scoped connection sees. It fixes the real blocker for the affected population: org-capable keys (personal API keys / OAuth withorganization:read) that pin a project and were silently deniedswitch-organization.Changes
Pinning now fixes the default active context without disabling navigation. Only an explicitly pinned organization is a hard lock (drop
switch-organization, keep project switching). A pinned project excludes nothing, so the full discover-then-switch flow works from an active project.The derivation moved into a small pure function so the intent is explicit and testable:
switch-organizationswitch-organizationswitch-organizationNote
This does not weaken project-scoped-key isolation.
switch-organizationrequiresorganization:read, whichgetToolsForFeaturesalready strips forscoped_teamskeys, independent of this logic.How did you test this code?
Automated only - I (Claude) did not run a live multi-org MCP session.
tests/unit/switch-tools-to-exclude.test.ts- covers the derivation the resolver had no unit coverage for. The regression it catches: if a pinned project ever re-excludesswitch-organization/switch-project, the documented cross-org flow silently breaks. Existingtool-filteringtests only exercise the filter given an explicitexcludeToolsarray; they never tested how that array is built from the pinned context.tests/unit/tool-filtering.test.tsandtests/integration/feature-routing.test.ts: 91 passed.feature-routingalready asserts theworkspacefeature exposesorganizations-get/switch-organization/projects-get/switch-project, so tool registration stays verified.Automatic notifications
Docs update
No docs changes - the MCP tool catalog is generated from tool definitions and the behavior lives in the resolver.
🤖 Agent context
Autonomy: Fully autonomous
Investigated and implemented by Claude (Opus 4.8) via PostHog Code, from an inbox report that org discovery/switch tools were still missing after #71976. I reproduced the symptom against the live catalog and traced it to two independent filters: the
scopedTeamsstrip (correct - project-scoped key) and theexcludeToolsproject-pin logic (the real bug). I confirmed the reporting session used a project-scoped key, so #71976 was never the relevant lever there; the fix targets org-capable keys instead, matching the report's stated user population.Considered but rejected: relaxing the
scopedTeamsstrip (would advertise tools the backend 403s), and adding a discoverability hint for project-scoped keys (useful, but a separate concern - kept this PR focused on the navigation bug). Skill invoked:/writing-tests.Created with PostHog Code from this inbox report.