From 5fa59e61d3150a07f1290bc4c019b60fc620c6fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vi=CC=81ctor=20Falco=CC=81n?= Date: Fri, 17 Jul 2026 18:26:05 +0200 Subject: [PATCH] harden(mcp): enforce OAuth read-only server-side, not via scope Block write tools whenever the request authenticated through the api (Passport) guard, independent of the scopes a client requested. Previously read-only relied on clients asking only for mcp:use and on tokenCan(); since Passport always honours the '*' wildcard scope, a client that requested '*' could have slipped past. Clamping on the resolving guard makes the locked 'OAuth = read-only' guarantee hold on the server. --- app/Mcp/Tools/WriteTool.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/Mcp/Tools/WriteTool.php b/app/Mcp/Tools/WriteTool.php index ab7b5057..c0da9b8d 100644 --- a/app/Mcp/Tools/WriteTool.php +++ b/app/Mcp/Tools/WriteTool.php @@ -10,6 +10,7 @@ use App\Models\Space; use App\Models\Transaction; use App\Models\User; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Facades\Auth; use Illuminate\Validation\ValidationException; use Laravel\Mcp\Request; use Laravel\Mcp\Response; @@ -27,6 +28,16 @@ abstract class WriteTool extends McpTool { protected function respond(Request $request, User $user): Response { + // OAuth connections (Claude Desktop / ChatGPT) authenticate through the + // `api` (Passport) guard and are read-only in this iteration, whatever + // scope a client requests: writes need a Claude Code read & write + // personal access token. Clamping on the resolving guard keeps the + // guarantee server-side instead of trusting the client to request a + // narrow scope (Passport always honours the `*` wildcard scope). + if (Auth::getDefaultDriver() === 'api') { + return Response::error('OAuth connections are read-only. Use a Claude Code read & write token to make changes.'); + } + if (! $user->tokenCan('mcp:write')) { return Response::error('This token is read-only. Create a read & write token to make changes.'); }