## 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. |
||
|---|---|---|
| .agents/skills | ||
| .claude | ||
| .cursor | ||
| .github | ||
| .opencode/skills | ||
| .pi | ||
| app | ||
| bootstrap | ||
| config | ||
| database | ||
| docker | ||
| docs | ||
| experiments | ||
| lang | ||
| public | ||
| resources | ||
| routes | ||
| screenshots | ||
| scripts | ||
| src/lib/crypto | ||
| storage | ||
| templates/coolify | ||
| tests | ||
| .dockerignore | ||
| .editorconfig | ||
| .env.example | ||
| .env.production.example | ||
| .gitattributes | ||
| .gitignore | ||
| .mcp.json | ||
| .php-cs-fixer.dist.php | ||
| .php-version | ||
| .prettierignore | ||
| .prettierrc | ||
| .release-it.json | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| Dockerfile | ||
| Dockerfile.production | ||
| LICENSE.md | ||
| LOCALIZATION.md | ||
| ONBOARDING.md | ||
| README.md | ||
| artisan | ||
| autoresearch-dashboard.md | ||
| autoresearch.jsonl | ||
| autoresearch.md | ||
| autoresearch.sh | ||
| boost.json | ||
| bun.lock | ||
| components.json | ||
| compose.yaml | ||
| composer.json | ||
| composer.lock | ||
| docker-compose.production.yml | ||
| eslint.config.js | ||
| falcode.json | ||
| opencode.json | ||
| package-lock.json | ||
| package.json | ||
| phpstan-baseline.neon | ||
| phpstan.neon | ||
| phpunit.xml | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
| vitest.setup.ts | ||
| whispermoney | ||
| worktree.sh | ||
README.md
Deutsch | Español | français | 日本語 | 한국어 | Português | Русский | 中文
Whisper Money
The most secure way to understand your finances.
Whisper Money is a privacy-first personal finance application that helps you track, categorize, and understand your spending—all while keeping your financial data encrypted and secure.
🎮 Try the Demo: Experience Whisper Money with our demo account - no registration required!
💬 Join our Community: Whether you're a user looking for help or a developer wanting to contribute, we'd love to have you in our Discord server! Share feedback, ask questions, discuss new features, or just hang out with fellow privacy enthusiasts.
Features
- 🔐 Privacy-first — Your data is never shared with third parties. You own it
- 🏦 Bank account management — Track multiple accounts in one place
- 📊 Transaction categorization — Automatic and manual categorization
- 🤖 Automation rules — Set up rules to auto-categorize transactions
- 📈 Financial insights — Understand your spending patterns
Tech Stack
- Backend: Laravel 12, PHP 8.4
- Frontend: React 19, Inertia.js v2, TypeScript
- Styling: Tailwind CSS v4
- Database: MySQL
- Cache/Queue: Redis
- Testing: Pest v4
Running Locally
Quick Start (Recommended)
The easiest way to get started is using our automated setup script:
bash <(curl -fsSL https://whisper.money/setup.sh)
After installation, just visit https://whisper.money.localhost in your browser.
Manual Setup
If you prefer to set up manually:
- Clone the repository:
git clone https://github.com/whisper-money/whisper-money.git
cd whisper-money
- Run the setup script:
whispermoney install
Available Commands
Important: You must run
whispermoney installbefore using any other command. If you skip the install step, commands likestartwill not work.
Once installed, you can use the whispermoney command for common tasks:
# Start all services
whispermoney start
# Stop all services
whispermoney stop
# Upgrade to latest version
whispermoney upgrade
# Interactive menu
whispermoney
Development Server
For active development with hot reloading:
composer run dev
This will concurrently start:
- PHP development server (via Portless HTTPS proxy)
- Queue worker
- Log viewer (Pail)
- Vite dev server
The application will be available at https://dev.whisper.money.localhost. In git worktrees, the branch name is automatically prepended (e.g. https://fix-ui.dev.whisper.money.localhost).
Running with Docker (Production Image)
For testing the production Docker image locally:
- Copy the production environment file:
cp .env.production.example .env
- Start the services:
docker compose -f docker-compose.production.yml up -d
The application will be available at http://localhost:8080.
To use a different port, set APP_PORT:
APP_PORT=3000 docker compose -f docker-compose.production.yml up -d
Deploying to Coolify
Whisper Money can be easily deployed to Coolify using our Docker Compose template.
Quick Deploy
- In Coolify, create a new resource and select Docker Compose
- Choose Empty Compose File as the source
- Paste the contents from our template: 👉 whisper-money.yaml
- Deploy!
The template includes:
- Whisper Money application container
- MySQL 8.0 database with health checks
- Persistent volumes for data and storage
- Auto-generated database credentials
Required Environment Variables
| Variable | Description |
|---|---|
RESEND_API_KEY |
Email service API key (for password resets, notifications) |
Note:
APP_KEYandAPP_URLare auto-configured. The container generates anAPP_KEYon first startup if not provided.
Optional Environment Variables
| Variable | Default | Description |
|---|---|---|
DRIP_EMAILS_ENABLED |
true |
Enable drip emails (welcome, onboarding, feedback) |
HIDE_AUTH_BUTTONS |
false |
Hide login/register buttons on landing page |
SUBSCRIPTIONS_ENABLED |
false |
Enable Stripe subscriptions |
STRIPE_KEY |
- | Stripe publishable key |
STRIPE_SECRET |
- | Stripe secret key |
STRIPE_WEBHOOK_SECRET |
- | Stripe webhook signing secret |
Star History
License
This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.