Skip to content

[TW-7622] MCP-only principal-scoped login, credential isolation and OAuth status - #127

Draft
radenkovic wants to merge 1 commit into
codex/tw-7617-cli-devicefrom
dan/TW-7622-mcp-only-principal-login
Draft

radenkovic wants to merge 1 commit into
codex/tw-7617-cli-devicefrom
dan/TW-7622-mcp-only-principal-login

Conversation

@radenkovic

@radenkovic radenkovic commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Jira: TW-7622 (epic TW-7610). Stacked on #125 (TW-7617 device sign-in); retarget to main once that merges.

What

  • nylas oauth login --mcp-only (browser or --device): the --for mcp scopes without dashboard.session, so the consent screen can offer an IAM principal. There is no principal flag; the principal is chosen on the consent page.
  • Never a dashboard session: an MCP-only login skips the dashboard exchange. The exchange also refuses any token carrying a principal claim before calling the server. A dashboard-capable login whose consent returned a principal is revoked and refused rather than stored as the default session.
  • Credential isolation: each MCP-only credential is stored in its own keyring keys (oauth_mcp_<id>_*), beside the default session and never in place of it. The id is a hash of server, audience, org, user and principal, and a token-free index lists them. The same key replaces that credential and revokes the one it replaces; any other combination is stored alongside, so two principals or regions never overwrite each other.
  • Selection: nylas mcp serve --auth oauth uses the default session when present. Otherwise it uses the only MCP-only credential, and with several it requires --credential <id>. mcp install --credential pins one and preserves it on re-install.
  • Status/help: oauth status shows access mode, audience, org and principal for the default session and every MCP-only credential, never a token. Also adds oauth token --credential, oauth logout --credential / --all; config reset clears every credential.
  • 503 on refresh: retries temporarily_unavailable after Retry-After (capped at 30s) with backoff, up to 3 attempts, with the same refresh token, and keeps the stored sign-in (contract §7).

Tests

  • Unit and fake-server tests cover requested scopes, storage selection, two principals not overwriting each other, replacement and revocation, refusing tokens it cannot store safely, the exchange refusal, and the 503 retry and its bounds.
  • Integration: TestCLI_OAuthMCPOnly_FakeServerLogin (device + --mcp-only against a fake AS: no dashboard.session requested, no exchange call, status and token --credential).
  • Two deliberate code breaks (ignoring MCPOnly; disabling the retry) each failed the new tests.
  • make fmt vet lint test-unit test-race security build passes. make vuln fails on the Go 1.26.5 stdlib pinned in go.mod, and fails the same way on main.
  • Not tested against a live authorization server (TestOAuthAS_* skip without one).

Notes for review

  • Tokens carry no authorization-id claim. Status shows the identifying fields, and the credential id is derived from them.
  • With several MCP-only credentials and no default session, mcp serve exits with an error rather than choosing one.

🤖 Generated with Claude Code

…ation and OAuth status

`nylas oauth login --mcp-only` requests the MCP scopes without
dashboard.session, so the consent screen can offer an IAM principal. The
result is stored as its own credential beside the default session, keyed by
server, audience, org, user and principal: the same key replaces (and revokes)
that credential, anything else is stored alongside it. A restricted token is
never exchanged for a dashboard session, and the exchange refuses any token
carrying a principal claim before calling the server.

- oauth status lists the default session and every MCP-only credential with
  access mode, audience, org and principal; no token is printed
- oauth token/logout --credential, logout --all; config reset clears all
- mcp serve/install --auth oauth --credential <id>; without it serve uses the
  default session, else the only MCP-only credential
- refresh retries 503 temporarily_unavailable after Retry-After with backoff
  and keeps the stored sign-in (contract section 7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant