feat(mcp): cut over from Hub OAuth to local accounts #122

Merged
Claude-Run merged 1 commit from feat/YT-69-local-accounts-cutover into main 2026-07-17 16:08:19 +02:00
Member

yt mcp serve authenticated every browser against YouTrack Hub and reused each user's Hub access token as their YouTrack REST bearer, so the service's identity, its allowlist, and its only YouTrack credential were all one upstream grant. This replaces that with the local accounts YT-67 added, and gives each account its own YouTrack instance. YT-68 was the schema half of the same cut and is absorbed here: the two deadlock (dropping users forces deleting oauth_hub.rs, and repointing client_for_user needs youtrack_instances to exist), so they are one commit or neither.

Schema: migration 0004 adds youtrack_instances, keyed one-to-one on accounts(id) with ON DELETE CASCADE (the primary key is the foreign key, so 0-or-1 is structural), holding the account's base URL plus its permanent YouTrack token as AEAD ciphertext under YOUTRACK_MCP_MASTER_KEY. hub_tokens, allowlist, and users are dropped; access_tokens, refresh_tokens, and authorization_codes keep their shape with their principal repointed at accounts(id). The column is renamed user_id -> account_id in the same change: users.id and accounts.id were independent AUTOINCREMENT sequences, so a mistaken write across them would have bound a token to an unrelated Hub user, and after this there is no users(id) column left to write into. No data is carried over, per YT-66.

Authorization Server: /authorize sends an unauthenticated browser to /login with the pending request stashed in the signed session cookie, and the local sign-in (password, TOTP, or a recovery code) resumes it. Dynamic client registration, PKCE, codes, access/refresh tokens, consent, and the RS middleware are untouched. oauth_hub.rs, the /oauth/hub/callback route, hub_auth.rs, hub_scope.rs, allowlist.rs, and the oauth2 dependency are gone, and with them the defect class they generated: the scope trap, the client secret and its rotation, and Hub token liveness and recovery.

client_for_account builds a client from the account's own base URL and decrypted token, with no expiry check and no refresh path: a permanent token does not expire and cannot be re-minted, so the retry and diagnosis machinery in server.rs goes too. An account with no instance is a fresh account, not a fault, and its tool calls come back naming the page where it adds one.

yt mcp serve no longer reads YOUTRACK_BASE_URL (the base URL is per-account data now); yt mcp stdio still does and is unchanged. No YOUTRACK_MCP_HUB_* variable is read anywhere, and none remain in .env.example, compose.mcp.yml, or docs/deployment.md. yt mcp secret rotate, yt mcp allowlist, yt mcp auth, and yt mcp hub are removed: each exists only to manage a Hub credential or a table this drops. yt mcp service install bakes the local-account configuration instead.

The admin UI is repointed at accounts (list, ban and revoke, settings); an account's instance is deliberately not readable there. A minimal /instance page is added so an account can supply its own base URL and token, which is what makes any tool call possible at all; the fuller settings and admin surfaces stay tracked in YT-70 and YT-71, and the deny list over local accounts in YT-65.

it_oauth.rs drops every Hub-driven test and drives the whole dance against a local account through the real endpoints, ending in an MCP tools/call against a stubbed YouTrack that accepts only that account's own token.

#YT-69

`yt mcp serve` authenticated every browser against YouTrack Hub and reused each user's Hub access token as their YouTrack REST bearer, so the service's identity, its allowlist, and its only YouTrack credential were all one upstream grant. This replaces that with the local accounts YT-67 added, and gives each account its own YouTrack instance. YT-68 was the schema half of the same cut and is absorbed here: the two deadlock (dropping `users` forces deleting `oauth_hub.rs`, and repointing `client_for_user` needs `youtrack_instances` to exist), so they are one commit or neither. Schema: migration 0004 adds `youtrack_instances`, keyed one-to-one on `accounts(id)` with ON DELETE CASCADE (the primary key is the foreign key, so 0-or-1 is structural), holding the account's base URL plus its permanent YouTrack token as AEAD ciphertext under YOUTRACK_MCP_MASTER_KEY. `hub_tokens`, `allowlist`, and `users` are dropped; `access_tokens`, `refresh_tokens`, and `authorization_codes` keep their shape with their principal repointed at `accounts(id)`. The column is renamed `user_id` -> `account_id` in the same change: `users.id` and `accounts.id` were independent AUTOINCREMENT sequences, so a mistaken write across them would have bound a token to an unrelated Hub user, and after this there is no `users(id)` column left to write into. No data is carried over, per YT-66. Authorization Server: `/authorize` sends an unauthenticated browser to `/login` with the pending request stashed in the signed session cookie, and the local sign-in (password, TOTP, or a recovery code) resumes it. Dynamic client registration, PKCE, codes, access/refresh tokens, consent, and the RS middleware are untouched. `oauth_hub.rs`, the `/oauth/hub/callback` route, `hub_auth.rs`, `hub_scope.rs`, `allowlist.rs`, and the `oauth2` dependency are gone, and with them the defect class they generated: the scope trap, the client secret and its rotation, and Hub token liveness and recovery. `client_for_account` builds a client from the account's own base URL and decrypted token, with no expiry check and no refresh path: a permanent token does not expire and cannot be re-minted, so the retry and diagnosis machinery in `server.rs` goes too. An account with no instance is a fresh account, not a fault, and its tool calls come back naming the page where it adds one. `yt mcp serve` no longer reads YOUTRACK_BASE_URL (the base URL is per-account data now); `yt mcp stdio` still does and is unchanged. No YOUTRACK_MCP_HUB_* variable is read anywhere, and none remain in .env.example, compose.mcp.yml, or docs/deployment.md. `yt mcp secret rotate`, `yt mcp allowlist`, `yt mcp auth`, and `yt mcp hub` are removed: each exists only to manage a Hub credential or a table this drops. `yt mcp service install` bakes the local-account configuration instead. The admin UI is repointed at accounts (list, ban and revoke, settings); an account's instance is deliberately not readable there. A minimal `/instance` page is added so an account can supply its own base URL and token, which is what makes any tool call possible at all; the fuller settings and admin surfaces stay tracked in YT-70 and YT-71, and the deny list over local accounts in YT-65. `it_oauth.rs` drops every Hub-driven test and drives the whole dance against a local account through the real endpoints, ending in an MCP `tools/call` against a stubbed YouTrack that accepts only that account's own token. #YT-69
feat(mcp): cut over from Hub OAuth to local accounts
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 1m50s
Create release / Create release from merged PR (pull_request) Has been skipped
21664ae9d4
`yt mcp serve` authenticated every browser against YouTrack Hub and reused each user's Hub access token as their YouTrack REST bearer, so the service's identity, its allowlist, and its only YouTrack credential were all one upstream grant. This replaces that with the local accounts YT-67 added, and gives each account its own YouTrack instance. YT-68 was the schema half of the same cut and is absorbed here: the two deadlock (dropping `users` forces deleting `oauth_hub.rs`, and repointing `client_for_user` needs `youtrack_instances` to exist), so they are one commit or neither.

Schema: migration 0004 adds `youtrack_instances`, keyed one-to-one on `accounts(id)` with ON DELETE CASCADE (the primary key is the foreign key, so 0-or-1 is structural), holding the account's base URL plus its permanent YouTrack token as AEAD ciphertext under YOUTRACK_MCP_MASTER_KEY. `hub_tokens`, `allowlist`, and `users` are dropped; `access_tokens`, `refresh_tokens`, and `authorization_codes` keep their shape with their principal repointed at `accounts(id)`. The column is renamed `user_id` -> `account_id` in the same change: `users.id` and `accounts.id` were independent AUTOINCREMENT sequences, so a mistaken write across them would have bound a token to an unrelated Hub user, and after this there is no `users(id)` column left to write into. No data is carried over, per YT-66.

Authorization Server: `/authorize` sends an unauthenticated browser to `/login` with the pending request stashed in the signed session cookie, and the local sign-in (password, TOTP, or a recovery code) resumes it. Dynamic client registration, PKCE, codes, access/refresh tokens, consent, and the RS middleware are untouched. `oauth_hub.rs`, the `/oauth/hub/callback` route, `hub_auth.rs`, `hub_scope.rs`, `allowlist.rs`, and the `oauth2` dependency are gone, and with them the defect class they generated: the scope trap, the client secret and its rotation, and Hub token liveness and recovery.

`client_for_account` builds a client from the account's own base URL and decrypted token, with no expiry check and no refresh path: a permanent token does not expire and cannot be re-minted, so the retry and diagnosis machinery in `server.rs` goes too. An account with no instance is a fresh account, not a fault, and its tool calls come back naming the page where it adds one.

`yt mcp serve` no longer reads YOUTRACK_BASE_URL (the base URL is per-account data now); `yt mcp stdio` still does and is unchanged. No YOUTRACK_MCP_HUB_* variable is read anywhere, and none remain in .env.example, compose.mcp.yml, or docs/deployment.md. `yt mcp secret rotate`, `yt mcp allowlist`, `yt mcp auth`, and `yt mcp hub` are removed: each exists only to manage a Hub credential or a table this drops. `yt mcp service install` bakes the local-account configuration instead.

The admin UI is repointed at accounts (list, ban and revoke, settings); an account's instance is deliberately not readable there. A minimal `/instance` page is added so an account can supply its own base URL and token, which is what makes any tool call possible at all; the fuller settings and admin surfaces stay tracked in YT-70 and YT-71, and the deny list over local accounts in YT-65.

`it_oauth.rs` drops every Hub-driven test and drives the whole dance against a local account through the real endpoints, ending in an MCP `tools/call` against a stubbed YouTrack that accepts only that account's own token.

#YT-69
Claude-Run deleted branch feat/YT-69-local-accounts-cutover 2026-07-17 16:08:20 +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!122
No description provided.