MCP: switch-organization commits unvalidated state and reports success on a failed org fetch
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 39.9k
- Forks
- 3.4k
- Avg merge
- 6h 51m
- Merged PRs (30d)
- 232
Description
Summary
switch-organization writes orgId to the session cache before (and independently of) fetching the organization, and returns a success string whether or not that fetch succeeded. switch-project had the same shape, was identified as a bug, and was fixed — the fix was not applied to the organization tool.
services/mcp/src/tools/organizations/setActive.ts:
const { orgId } = params
await context.cache.set('orgId', orgId) // committed unconditionally
let org: CachedOrg | undefined
const orgResult = await context.api.organizations().get({ orgId })
if (orgResult.success) { // failure silently ignored
org = orgResult.data
...
}
...
: `Switched to organization ${orgId}` // success either way
Compare services/mcp/src/tools/projects/setActive.ts:28-43, whose comment describes precisely this failure mode:
Validate before committing the session: only switch to a project the user can actually access. Previously the projectId was cached before the fetch, so a bad id (or a project the session can't reach) silently "succeeded" and every later call failed with an opaque error instead.
That validation landed in #71976 for projects only.
Impact
An agent calling switch-organization with an id it cannot access is told the switch worked. Nothing contradicts that: subsequent org-nested reads fail with opaque 403/404s that don't mention the switch, and the agent has no reason to suspect the org it believes it is in. The stale orgId also persists in the token-keyed cache (7-day TTL) rather than being rolled back.
For clients that pin x-posthog-organization-id, the resolver re-pins on every request so the drift is corrected immediately. For clients that don't pin, it persists.
To be clear about severity: this is a correctness/UX problem, not an access-control one. Django enforces the token's scoped_organizations on org-nested views, so an out-of-scope org id yields failed requests rather than data. The issue is that the MCP layer reports success for something that didn't happen.
Suggested fix
Mirror projects/setActive.ts: fetch first, throw wrapError(...) on failure with a message naming the recovery path, and only cache.set('orgId', ...) once the fetch succeeds.
Notes
Verified by reading source at 34c5044330427. Found while building an org-scoped OAuth integration against mcp.posthog.com.
Possibly related: with a project pinned via x-posthog-project-id, switch-project is advertised but inert — the header re-pins projectId on every request (request-state-resolver.ts:121-126), so the tool reports an honest success and the next read still comes from the pinned project. #73023 stopped excluding switch-project from the roster when only a project is pinned, but the pin still overrides it. Happy to split that into its own issue if it's not the same discussion.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with services/mcp/src/tools/organizations/setActive.ts and compare it with services/mcp/src/tools/projects/setActive.ts:28-43, then inspect the organization fetch and cache update flow. Done means an inaccessible or invalid organization does not update the session cache or report success, while a successful fetch switches the active organization. Confirm the behavior against the failure scenario described in the issue.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 82/100