whisper-money/database/migrations
Víctor Falcón 6d5f440727
feat(mcp): add OAuth 2.1 for Claude Desktop & ChatGPT connectors (Phase 3) (#691)
## MCP Phase 3 — OAuth 2.1 for Claude Desktop/web & ChatGPT connectors

Phase 1 shipped a read-only MCP server (#689); Phase 2 added write tools
+ the read/read_write token scope (#690). This phase adds **OAuth 2.1
(Authorization Code + PKCE)** so Anthropic's Claude Desktop/web custom
connectors and OpenAI's ChatGPT connectors can authenticate — those
clients sign in with OAuth rather than pasting a static bearer token, so
until now they only saw a "coming soon" note.

It reuses `laravel/mcp`'s built-in OAuth support (inert until Passport
is installed) wired to `laravel/passport ^13`. We do not hand-write the
authorization server, discovery endpoints, DCR endpoint, or the
`WWW-Authenticate` challenge — the package provides all of it.

### What's in it
- **`laravel/passport ^13`** + an `api` (passport) guard alongside the
existing session `web` guard; Passport migrations (UUID user columns),
config, and signing keys.
- **`Mcp::oauthRoutes()`** — RFC 8414/9728 discovery, RFC 7591 DCR
(`oauth/register`), and the `mcp:use` scope.
- **A second MCP endpoint `POST /mcp/oauth`** guarded by `auth:api`. The
existing Sanctum `/mcp` endpoint (Claude Code static PAT) is left 100%
unchanged.
- **On-brand OAuth consent screen** (Blade, light + dark, localized)
naming the connecting client and its redirect host, and stating plainly
what the connection can do (read/analyse + make changes, bank-connected
data excepted).
- **Settings UI**: the "Claude Desktop & ChatGPT" block now shows real
connect instructions (the `/mcp/oauth` URL to add as a custom connector,
no token needed) instead of "coming soon".

## Decision #1 — OAuth connections have read + write access

`laravel/mcp` advertises and uses a single `mcp:use` scope; it has no
read/write granularity, so there is no per-connection scope choice over
OAuth. **OAuth connections get full read + write access**, gated by the
user explicitly approving the connection on the Whisper Money consent
screen. (An earlier revision made them read-only; that restriction has
been lifted per request.)

`WriteTool` (`app/Mcp/Tools/WriteTool.php`) grants writes when the
request resolves through the `api` (Passport) guard **or** carries a
Sanctum `mcp:write` ability; a read-only Sanctum PAT is still rejected.
Bank-connected accounts and their transactions remain read-only for
every caller (only manual data can be created/edited/deleted; any
transaction can still be categorised/labelled). The consent screen and
settings copy state the read + write capability and the bank-connected
exception.

Possible follow-up: a consent-time read-only/read-write toggle, if
per-connection granularity is wanted (not offered by the standard MCP
OAuth flow's single scope).

## Other locked decisions
- **Route topology**: a separate `/mcp/oauth` endpoint rather than
multi-guarding `/mcp`. Keeps the Claude Code path unchanged (its
`abilities:mcp:read` gate would 403 an OAuth `mcp:use` token) and gives
each client type a clean documented URL. The package's nested discovery
`/.well-known/oauth-protected-resource/mcp/oauth` returns `resource =
url('/mcp/oauth')`.
- **Registration**: ship DCR (`oauth/register`). Redirect allowlist
tightened to `https://claude.ai` and `https://chatgpt.com` only — no
wildcard. CIMD is a possible later enhancement; both clients accept DCR.

## Deviation from the original plan — the User model is untouched
The plan proposed aliasing Passport's `HasApiTokens` trait alongside
Sanctum's (with `insteadof`/`as`) and implementing `OAuthenticatable`.
**Both are impossible here and, it turns out, unnecessary:**
- The two `HasApiTokens` traits declare an **incompatible `$accessToken`
property** (Sanctum untyped vs Passport `?ScopeAuthorizable`), which is
a hard PHP fatal that `insteadof` cannot resolve (it only resolves
methods).
- `OAuthenticatable::tokens(): HasMany` is incompatible with Sanctum's
canonical `tokens(): MorphMany`, and the Claude Code PAT suite depends
on Sanctum's `tokens()`. The interface is never enforced at runtime by
Passport (docblock-only).
- Passport's resource guard only calls `$user->withAccessToken()`, which
Sanctum already provides (untyped, so it accepts the Passport
`AccessToken`); and Passport's `AccessToken::can()` makes
`tokenCan('mcp:write')` behave correctly for OAuth tokens. So Sanctum
stays canonical and the Claude Code PAT path is genuinely unchanged.

## Signing keys (deploy note)
Passport signs OAuth tokens with a key pair. This PR provisions it
everywhere it's needed: CI (`passport:keys` before tests), the
production Docker entrypoint (generates into the persisted `storage/`
volume unless provided via `PASSPORT_PRIVATE_KEY`/`PASSPORT_PUBLIC_KEY`
env), `worktree.sh`, and a documented `.env.example` entry. **For a
multi-instance deployment, set `PASSPORT_*` env** so every instance
validates tokens with the same key.

## Tests (`tests/Feature/Mcp/McpOAuthTest.php`)
Discovery metadata (RFC 9728/8414), the mandatory **401 bootstrap**
challenge + `WWW-Authenticate` header, DCR (allowed + rejected redirect
URIs), the full **Authorization Code + PKCE** flow reaching a read tool,
and **write access over OAuth** (an OAuth connection calling
`create_label` succeeds and the row is created). The existing
`McpTokenTest` / `Mcp/*` suites (incl. the read-only Sanctum PAT
guardrail) and `LocalizationTest` still pass unchanged.

## QA
- **Protocol** (curl, over HTTPS): both discovery endpoints return the
exact required JSON; unauthenticated `POST /mcp/oauth` returns `401` +
`WWW-Authenticate: Bearer …
resource_metadata="…/.well-known/oauth-protected-resource/mcp/oauth"`;
DCR accepts `claude.ai`/`chatgpt.com` callbacks and rejects others with
`400 invalid_redirect_uri`.
- **Browser**: consent screen verified in light and dark mode (client
name, signed-in email, redirect host, read + write capability +
bank-connected read-only note, Cancel/Connect); updated settings page
verified. No JS errors.
- Full PKCE token exchange + a write tool call is covered by the green
Pest e2e test.

## Fast-follows (not in this PR)
- **"Connected apps" revoke UI** — `McpTokenController` manages only
Sanctum PATs today, so there's no in-app revoke for OAuth grants yet.
The consent copy says "disconnect from the connected app" for now; a
Passport-grant list + revoke is the top follow-up (more important now
that OAuth grants can write).
- CIMD registration; optional consent-time read-only/read-write toggle.

## Stacking
Was developed stacked on `mcp-write-tools` (#690), itself on #689.
**Both have since merged to `main`**, so this branch was rebased onto
`main` (`git rebase --onto origin/main mcp-write-tools`) and targets
`main` directly.
2026-07-17 19:10:48 +02:00
..
0001_01_01_000000_create_users_table.php
0001_01_01_000001_create_cache_table.php
0001_01_01_000002_create_jobs_table.php
2025_08_26_100418_add_two_factor_columns_to_users_table.php
2025_11_07_135255_add_encryption_salt_to_users_table.php
2025_11_07_135256_create_encrypted_messages_table.php
2025_11_07_150038_create_banks_table.php
2025_11_07_150122_create_accounts_table.php
2025_11_07_150613_create_categories_table.php
2025_11_07_150659_create_transactions_table.php
2025_11_07_184056_modify_banks_table_add_user_id_and_simplify_name.php
2025_11_07_185018_change_banks_logo_to_text.php
2025_11_08_140230_change_transactions_id_to_uuid.php
2025_11_08_141344_make_category_id_nullable_in_transactions_table.php
2025_11_08_144530_add_soft_deletes_to_transactions_table.php
2025_11_10_110100_create_automation_rules_table.php
2025_11_15_172640_create_account_balances_table.php
2025_11_15_175636_change_transactions_amount_to_bigint.php
2025_11_15_195739_convert_all_ids_to_uuid.php
2025_11_18_184409_create_user_leads_table.php
2025_11_28_104227_add_source_to_transactions_table.php
2025_11_29_170353_add_type_to_categories_table.php
2025_11_29_170955_fix_account_balances_unique_constraint.php
2025_12_01_101443_add_unique_constraint_to_categories_table.php
2025_12_06_112515_create_customer_columns.php
2025_12_06_112516_create_subscriptions_table.php
2025_12_06_112517_create_subscription_items_table.php
2025_12_06_112518_add_meter_id_to_subscription_items_table.php
2025_12_06_112519_add_meter_event_name_to_subscription_items_table.php
2025_12_10_142006_add_onboarded_at_to_users_table.php
2025_12_12_092647_create_labels_table.php
2025_12_12_092650_create_label_transaction_table.php
2025_12_12_092651_create_automation_rule_labels_table.php
2025_12_12_141955_update_labels_unique_constraint_include_deleted_at.php
2025_12_16_151952_create_user_mail_logs_table.php
2025_12_19_092437_create_budgets_table.php
2025_12_19_092443_create_budget_periods_table.php
2025_12_19_092446_create_budget_transactions_table.php
2025_12_20_154221_create_features_table.php
2025_12_29_063338_add_currency_code_to_users_table.php
2026_01_08_192757_add_email_identifier_to_user_mail_logs.php
2026_01_08_204223_update_user_mail_logs_unique_constraint.php
2026_01_21_151353_add_indexes_for_budget_transaction_assignment.php
2026_01_22_071325_add_processing_historical_to_budget_periods.php
2026_01_22_143232_add_locale_to_users_table.php
2026_02_08_103953_create_user_settings_table.php
2026_02_09_110624_add_encrypted_to_accounts_table.php
2026_02_09_131403_make_description_iv_nullable_on_transactions.php
2026_02_09_144915_create_banking_connections_table.php
2026_02_09_144959_add_banking_connection_fields_to_accounts_table.php
2026_02_09_144959_add_external_transaction_id_to_transactions_table.php
2026_02_10_085759_add_aspsp_logo_to_banking_connections_table.php
2026_02_10_095639_add_raw_data_to_transactions_table.php
2026_02_11_113115_add_pending_accounts_data_to_banking_connections_table.php
2026_02_11_113119_add_linked_at_to_accounts_table.php
2026_02_13_083716_add_original_description_to_transactions_table.php
2026_02_18_084241_add_api_token_to_banking_connections_table.php
2026_02_18_110556_add_api_secret_to_banking_connections_table.php
2026_02_20_074846_create_exchange_rates_table.php
2026_02_22_180317_add_invested_amount_to_account_balances_table.php
2026_02_24_193117_fix_budget_transaction_refund_amounts.php
2026_03_03_114620_add_paywall_seen_at_to_users_table.php
2026_03_04_085843_add_waitlist_columns_to_user_leads_table.php
2026_03_04_094926_add_locale_to_user_leads_table.php
2026_03_12_095411_add_iban_to_accounts_table.php
2026_03_18_112307_add_cashflow_direction_to_categories_table.php
2026_03_19_151039_add_include_loans_in_net_worth_chart_to_user_settings_table.php
2026_03_20_110659_create_real_estate_details_table.php
2026_03_20_132058_make_bank_id_nullable_on_accounts_table.php
2026_03_20_154755_add_include_real_estate_in_net_worth_chart_to_user_settings_table.php
2026_03_25_125717_add_revaluation_percentage_to_real_estate_details_table.php
2026_03_25_132244_create_loan_details_table.php
2026_03_30_123011_add_consecutive_sync_failures_to_banking_connections_table.php
2026_03_30_123011_create_banking_sync_logs_table.php
2026_04_14_120000_add_email_verification_to_user_leads_table.php
2026_04_16_063515_add_bank_transactions_email_cutoff_at_to_banking_connections_table.php
2026_04_16_092644_add_timezone_to_users_table.php
2026_04_22_084113_add_deleted_at_to_users_table.php
2026_04_25_092824_add_launch_columns_to_user_leads_table.php
2026_05_05_070442_add_rate_limited_until_to_banking_connections_table.php
2026_05_05_132023_convert_custom_budgets_to_monthly.php
2026_05_13_085027_add_dedup_fingerprint_to_transactions_table.php
2026_05_25_115100_update_default_saving_and_investment_category_types.php
2026_05_26_061232_add_reinvitation_columns_to_user_leads_table.php
2026_05_27_061513_add_counterparty_names_to_transactions_table.php
2026_05_28_073203_update_categories_unique_constraint_for_soft_deletes.php
2026_05_29_085835_update_saving_and_investment_category_cashflow_direction.php
2026_06_01_085554_create_budget_category_table.php
2026_06_01_085554_create_budget_label_table.php
2026_06_01_085554_migrate_budget_category_label_to_pivots.php
2026_06_02_090000_add_parent_id_to_categories_table.php
2026_06_02_094823_add_notify_on_bank_transactions_synced_to_user_settings_table.php
2026_06_05_120000_create_saved_filters_table.php
2026_06_05_185212_add_state_token_to_banking_connections_table.php
2026_06_08_114044_add_analysis_days_to_saved_filters_table.php
2026_06_09_124010_add_analysis_mode_to_saved_filters_table.php
2026_06_10_083644_add_last_logged_in_at_to_users_table.php
2026_06_10_084055_add_last_active_at_to_users_table.php
2026_06_12_134225_create_ai_consents_table.php
2026_06_12_134523_create_suggestion_runs_table.php
2026_06_12_134524_create_rule_suggestions_table.php
2026_06_13_000000_add_is_catch_all_to_budgets_table.php
2026_06_15_120911_add_ai_categorization_fields_to_transactions_table.php
2026_06_15_120911_add_origin_to_automation_rules_table.php
2026_06_15_120911_create_category_corrections_table.php
2026_06_17_081319_add_ai_suggested_category_to_transactions_table.php
2026_06_17_114601_create_integration_requests_table.php
2026_06_17_114602_create_integration_request_votes_table.php
2026_06_17_120850_add_status_to_integration_requests_table.php
2026_06_17_122001_seed_initial_integration_requests.php
2026_06_17_141827_add_comment_to_integration_requests_table.php
2026_06_18_092830_drop_unique_user_vote_from_integration_request_votes_table.php
2026_06_19_134854_create_stuck_cohort_snapshots_table.php
2026_06_20_105609_align_accounts_encrypted_flag_with_plaintext_names.php
2026_06_20_165235_add_position_to_accounts_table.php
2026_06_26_124609_add_ai_model_to_transactions_table.php
2026_06_27_000000_backfill_xxx_account_currencies.php
2026_06_27_114559_add_refunded_at_to_subscriptions_table.php
2026_06_27_154041_add_hidden_on_dashboard_to_accounts_table.php
2026_06_29_161916_add_transactions_last_visited_at_to_users_table.php
2026_07_01_064324_add_ai_consent_prompt_dismissed_at_to_users_table.php
2026_07_02_133321_add_user_source_created_at_index_to_transactions_table.php
2026_07_06_120000_create_spaces_table.php
2026_07_06_120001_create_space_user_table.php
2026_07_06_120002_create_space_invitations_table.php
2026_07_06_120003_add_current_space_id_to_users_table.php
2026_07_06_120004_add_space_id_to_owned_tables.php
2026_07_06_120005_backfill_spaces.php
2026_07_17_125642_create_personal_access_tokens_table.php feat(mcp): read-only MCP server for Pro accounts (#689) 2026-07-17 16:54:15 +02:00
2026_07_17_154027_create_oauth_auth_codes_table.php feat(mcp): add OAuth 2.1 for Claude Desktop & ChatGPT connectors (Phase 3) (#691) 2026-07-17 19:10:48 +02:00
2026_07_17_154028_create_oauth_access_tokens_table.php feat(mcp): add OAuth 2.1 for Claude Desktop & ChatGPT connectors (Phase 3) (#691) 2026-07-17 19:10:48 +02:00
2026_07_17_154029_create_oauth_refresh_tokens_table.php feat(mcp): add OAuth 2.1 for Claude Desktop & ChatGPT connectors (Phase 3) (#691) 2026-07-17 19:10:48 +02:00
2026_07_17_154030_create_oauth_clients_table.php feat(mcp): add OAuth 2.1 for Claude Desktop & ChatGPT connectors (Phase 3) (#691) 2026-07-17 19:10:48 +02:00
2026_07_17_154031_create_oauth_device_codes_table.php feat(mcp): add OAuth 2.1 for Claude Desktop & ChatGPT connectors (Phase 3) (#691) 2026-07-17 19:10:48 +02:00