fix(mcp): reject a Hub scope that cannot reach YouTrack #113

Merged
Claude-Run merged 1 commit from fix/YT-55-hub-scope-validation into main 2026-07-17 02:09:46 +02:00
Member

scope_has_youtrack_service_id asked only whether the scope carried a token that was neither 0-0-0-0-0 nor offline_access, so any string passed. The one it had to catch is YOUTRACK_MCP_HUB_CLIENT_ID: an operator putting the OAuth client id in the slot documented as <youtrack-service-id> sailed through the guard, and Hub then issued a well-formed token (aud: ["0-0-0-0-0", "youtrack-mcp"]) that YouTrack answers with 401 Invalid token on every call, forever, while Hub login, the OAuth dance, and token refresh all look healthy. The guard also ran only in install, so the hand-edited mcp.env that produced this was never checked at all.

The check now lives in youtrack_mcp::hub_scope and is shared by install and startup, so the two cannot drift. It has two modes, because 0-0-0-0-0 is genuinely ambiguous: it is YouTrack's own service on a bundled/self-hosted instance and Hub on InCloud. Given YouTrack's service id resolved from Hub the check is exact; without one it is syntactic (some service id beyond Hub's universal one, and not this server's client id), which is what a Hub that cannot be read degrades to.

yt mcp hub services lists Hub's services with id, name, applicationName, and homeUrl and marks YouTrack's, so the id is read off a list instead of copied out of a Hub URL. install resolves the same id and rejects a --hub-scope that misses it, naming the value to use, before writing any file. AppState::load re-checks at startup and refuses to boot. When a 401 still happens, the stored token's aud is decoded and, if it lacks the resolved service id, the caller is told the scope is missing YouTrack's service id and which one to configure, rather than getting YouTrack's opaque Invalid token passed through.

Reading Hub's service list uses the client-credentials grant, so docs/deployment.md now asks for that flow on the Hub Service and says what is lost by leaving it off. The docs and .env.example also state the bundled-vs-InCloud split for 0-0-0-0-0 rather than describing it only as "Hub's universal service id", which is true and is exactly what made the placeholder easy to fill in wrongly.

#YT-55

`scope_has_youtrack_service_id` asked only whether the scope carried a token that was neither `0-0-0-0-0` nor `offline_access`, so any string passed. The one it had to catch is `YOUTRACK_MCP_HUB_CLIENT_ID`: an operator putting the OAuth client id in the slot documented as `<youtrack-service-id>` sailed through the guard, and Hub then issued a well-formed token (`aud: ["0-0-0-0-0", "youtrack-mcp"]`) that YouTrack answers with `401 Invalid token` on every call, forever, while Hub login, the OAuth dance, and token refresh all look healthy. The guard also ran only in `install`, so the hand-edited `mcp.env` that produced this was never checked at all. The check now lives in `youtrack_mcp::hub_scope` and is shared by `install` and startup, so the two cannot drift. It has two modes, because `0-0-0-0-0` is genuinely ambiguous: it is YouTrack's own service on a bundled/self-hosted instance and Hub on InCloud. Given YouTrack's service id resolved from Hub the check is exact; without one it is syntactic (some service id beyond Hub's universal one, and not this server's client id), which is what a Hub that cannot be read degrades to. `yt mcp hub services` lists Hub's services with id, name, applicationName, and homeUrl and marks YouTrack's, so the id is read off a list instead of copied out of a Hub URL. `install` resolves the same id and rejects a `--hub-scope` that misses it, naming the value to use, before writing any file. `AppState::load` re-checks at startup and refuses to boot. When a 401 still happens, the stored token's `aud` is decoded and, if it lacks the resolved service id, the caller is told the scope is missing YouTrack's service id and which one to configure, rather than getting YouTrack's opaque `Invalid token` passed through. Reading Hub's service list uses the client-credentials grant, so docs/deployment.md now asks for that flow on the Hub Service and says what is lost by leaving it off. The docs and `.env.example` also state the bundled-vs-InCloud split for `0-0-0-0-0` rather than describing it only as "Hub's universal service id", which is true and is exactly what made the placeholder easy to fill in wrongly. #YT-55
fix(mcp): reject a Hub scope that cannot reach YouTrack
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 41s
Create release / Create release from merged PR (pull_request) Has been skipped
4c61af429d
`scope_has_youtrack_service_id` asked only whether the scope carried a token that was neither `0-0-0-0-0` nor `offline_access`, so any string passed. The one it had to catch is `YOUTRACK_MCP_HUB_CLIENT_ID`: an operator putting the OAuth client id in the slot documented as `<youtrack-service-id>` sailed through the guard, and Hub then issued a well-formed token (`aud: ["0-0-0-0-0", "youtrack-mcp"]`) that YouTrack answers with `401 Invalid token` on every call, forever, while Hub login, the OAuth dance, and token refresh all look healthy. The guard also ran only in `install`, so the hand-edited `mcp.env` that produced this was never checked at all.

The check now lives in `youtrack_mcp::hub_scope` and is shared by `install` and startup, so the two cannot drift. It has two modes, because `0-0-0-0-0` is genuinely ambiguous: it is YouTrack's own service on a bundled/self-hosted instance and Hub on InCloud. Given YouTrack's service id resolved from Hub the check is exact; without one it is syntactic (some service id beyond Hub's universal one, and not this server's client id), which is what a Hub that cannot be read degrades to.

`yt mcp hub services` lists Hub's services with id, name, applicationName, and homeUrl and marks YouTrack's, so the id is read off a list instead of copied out of a Hub URL. `install` resolves the same id and rejects a `--hub-scope` that misses it, naming the value to use, before writing any file. `AppState::load` re-checks at startup and refuses to boot. When a 401 still happens, the stored token's `aud` is decoded and, if it lacks the resolved service id, the caller is told the scope is missing YouTrack's service id and which one to configure, rather than getting YouTrack's opaque `Invalid token` passed through.

Reading Hub's service list uses the client-credentials grant, so docs/deployment.md now asks for that flow on the Hub Service and says what is lost by leaving it off. The docs and `.env.example` also state the bundled-vs-InCloud split for `0-0-0-0-0` rather than describing it only as "Hub's universal service id", which is true and is exactly what made the placeholder easy to fill in wrongly.

#YT-55
Claude-Run deleted branch fix/YT-55-hub-scope-validation 2026-07-17 02:09:46 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
pandoras-box/youtrack-cli!113
No description provided.