PostHog / PostHog/posthog

MCP: default project resolution stores "NaN" when the token's scoped org isn't the user's active org

Open Beginner friendly
#78,628 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug feature/mcp team/self-driving
Dominant language
Python
Stars
39.9k
Forks
3.4k
Avg merge
6h 51m
Merged PRs (30d)
232

Description

Summary

StateManager._getDefaultOrganizationAndProject calls Number() on a project object, producing NaN, which is then persisted as the string "NaN" and used as a project id for the life of the cache entry (7 days).

services/mcp/src/lib/StateManager.ts:170:

const projectsResult = await this._api.organizations().projects({ orgId: organizationId }).list()
if (projectsResult.success && projectsResult.data.length > 0) {
    return { organizationId, projectId: Number(projectsResult.data[0]!) }
}

projects().list() is typed Promise<Result<Schemas.ProjectBackwardCompat[]>> and returns result.data.results (services/mcp/src/api/client.ts:536-545). ProjectBackwardCompatBasic is an object (services/mcp/src/api/generated.ts:44560), so Number(...) is NaN.

The intent is clearly .id — the sibling branch a few lines earlier in the same function returns projectId: activeTeam.id.

Impact

setDefaultOrganizationAndProject guards with if (projectId !== undefined), and NaN !== undefined is true, so "NaN" is written to the token-scoped cache. getProjectId() then returns a truthy "NaN" and every project-nested request becomes:

  • GET /api/projects/NaN/...
  • POST /api/environments/NaN/mcp_tools/<tool>/

These 404. Because the value is cached (keyed on hash(bearer_token), 7-day TTL) and the default is only derived when no projectId is cached, it does not self-heal — every subsequent request on that token hits the same dead project id. The agent gets a bare 404 with NaN in the URL and no hint that switch-project would fix it, unlike the genuinely-missing-context path which returns the helpful MissingProjectContextError text.

When it fires

Only the fallback branch, i.e. when the token has scoped_organizations and the user's active organization is not among them (so the activeOrganization branch at StateManager.ts:148-155 is skipped). Concretely: an org-scoped OAuth token for a user who belongs to more than one organization and whose current organization isn't the scoped one.

Found while building an org-scoped OAuth integration against mcp.posthog.com.

Why CI doesn't catch it

services/mcp/tests/unit/StateManager.test.ts:200-206 mocks the list call as:

list: vi.fn().mockResolvedValue({ success: true, data: [789] }),

A bare number array, which the real client cannot return. With that mock Number(789) is 789 and the test passes.

Suggested fix
return { organizationId, projectId: Number(projectsResult.data[0]!.id) }

and update the mock to return project-shaped objects so the test exercises the real contract.

Notes

Introduced in #55953 (2026-04-23). list() already returned ProjectBackwardCompat[] at that commit, so this is not a regression from a later type change — the call has never matched its input type. Number() accepts any, so TypeScript doesn't flag it.

Verified by reading source at 34c5044330427. I have not run this against production.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in services/mcp/src/lib/StateManager.ts around line 170, then inspect the projects().list() contract in services/mcp/src/api/client.ts and the project shape in services/mcp/src/api/generated.ts. Update services/mcp/tests/unit/StateManager.test.ts around lines 200-206 so its mock matches the real response, run the StateManager tests, and verify the fallback never persists NaN as a project id.

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
Quiet
Clarity
Clearly specified
Newbie friendliness
86/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.