From bef94a9ce285469aa4f55e44b7374f9e36018320 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:42:19 +0200 Subject: [PATCH] feat(mcp): add per-team server toggle --- app/Http/Kernel.php | 2 ++ app/Http/Middleware/EnsureTeamMcpEnabled.php | 26 +++++++++++++++++++ app/Livewire/Team/Index.php | 5 ++++ app/Models/Team.php | 2 ++ ...d_is_mcp_server_enabled_to_teams_table.php | 22 ++++++++++++++++ .../livewire/settings/advanced.blade.php | 2 +- resources/views/livewire/team/index.blade.php | 2 ++ routes/ai.php | 2 +- .../Authorization/TeamAuthorizationTest.php | 12 +++++++++ tests/Feature/Mcp/McpEndpointTest.php | 10 +++++++ 10 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 app/Http/Middleware/EnsureTeamMcpEnabled.php create mode 100644 database/migrations/2026_06_25_000000_add_is_mcp_server_enabled_to_teams_table.php diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 02a49aaa8..9e8dee83e 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -12,6 +12,7 @@ use App\Http\Middleware\DecideWhatToDoWithUser; use App\Http\Middleware\EncryptCookies; use App\Http\Middleware\EnsureMcpEnabled; +use App\Http\Middleware\EnsureTeamMcpEnabled; use App\Http\Middleware\EnsureTokenBelongsToCurrentTeamMember; use App\Http\Middleware\PreventRequestsDuringMaintenance; use App\Http\Middleware\RedirectIfAuthenticated; @@ -110,5 +111,6 @@ class Kernel extends HttpKernel 'can.update.resource' => CanUpdateResource::class, 'can.access.terminal' => CanAccessTerminal::class, 'mcp.enabled' => EnsureMcpEnabled::class, + 'mcp.team.enabled' => EnsureTeamMcpEnabled::class, ]; } diff --git a/app/Http/Middleware/EnsureTeamMcpEnabled.php b/app/Http/Middleware/EnsureTeamMcpEnabled.php new file mode 100644 index 000000000..5c76d2a1b --- /dev/null +++ b/app/Http/Middleware/EnsureTeamMcpEnabled.php @@ -0,0 +1,26 @@ +user(); + $teamId = $user?->currentAccessToken()?->team_id; + + $team = $user?->teams() + ->where('teams.id', $teamId) + ->first(); + + if (! $team?->is_mcp_server_enabled) { + return response()->json(['message' => 'MCP server is disabled for this team.'], 403); + } + + return $next($request); + } +} diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php index 140d9f5cc..406d385da 100644 --- a/app/Livewire/Team/Index.php +++ b/app/Livewire/Team/Index.php @@ -24,11 +24,14 @@ class Index extends Component public ?string $description = null; + public bool $is_mcp_server_enabled = true; + protected function rules(): array { return [ 'name' => ValidationPatterns::nameRules(), 'description' => ValidationPatterns::descriptionRules(), + 'is_mcp_server_enabled' => 'boolean', ]; } @@ -58,10 +61,12 @@ private function syncData(bool $toModel = false): void // Sync TO model (before save) $this->team->name = $this->name; $this->team->description = $this->description; + $this->team->is_mcp_server_enabled = $this->is_mcp_server_enabled; } else { // Sync FROM model (on load/refresh) $this->name = $this->team->name; $this->description = $this->team->description; + $this->is_mcp_server_enabled = $this->team->is_mcp_server_enabled; } } diff --git a/app/Models/Team.php b/app/Models/Team.php index 23e2badb3..a979b44fb 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -47,10 +47,12 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen 'personal_team', 'show_boarding', 'custom_server_limit', + 'is_mcp_server_enabled', ]; protected $casts = [ 'personal_team' => 'boolean', + 'is_mcp_server_enabled' => 'boolean', ]; protected static function booted() diff --git a/database/migrations/2026_06_25_000000_add_is_mcp_server_enabled_to_teams_table.php b/database/migrations/2026_06_25_000000_add_is_mcp_server_enabled_to_teams_table.php new file mode 100644 index 000000000..3162a8613 --- /dev/null +++ b/database/migrations/2026_06_25_000000_add_is_mcp_server_enabled_to_teams_table.php @@ -0,0 +1,22 @@ +boolean('is_mcp_server_enabled')->default(true); + }); + } + + public function down(): void + { + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn('is_mcp_server_enabled'); + }); + } +}; diff --git a/resources/views/livewire/settings/advanced.blade.php b/resources/views/livewire/settings/advanced.blade.php index 544ed7d4c..fb7da30a7 100644 --- a/resources/views/livewire/settings/advanced.blade.php +++ b/resources/views/livewire/settings/advanced.blade.php @@ -72,7 +72,7 @@ class="flex flex-col h-full gap-8 sm:flex-row"> @endif

MCP Server

-
@if ($is_mcp_server_enabled) diff --git a/resources/views/livewire/team/index.blade.php b/resources/views/livewire/team/index.blade.php index 8f54a57e3..0fbbb114f 100644 --- a/resources/views/livewire/team/index.blade.php +++ b/resources/views/livewire/team/index.blade.php @@ -13,6 +13,8 @@
+ @can('update', $team) Save diff --git a/routes/ai.php b/routes/ai.php index 3a39677ff..d79ff0eba 100644 --- a/routes/ai.php +++ b/routes/ai.php @@ -4,4 +4,4 @@ use Laravel\Mcp\Facades\Mcp; Mcp::web('/mcp', CoolifyServer::class) - ->middleware(['mcp.enabled', 'auth:sanctum', 'api.token.team']); + ->middleware(['mcp.enabled', 'auth:sanctum', 'api.token.team', 'mcp.team.enabled']); diff --git a/tests/Feature/Authorization/TeamAuthorizationTest.php b/tests/Feature/Authorization/TeamAuthorizationTest.php index e99e55a2c..6cfc26d99 100644 --- a/tests/Feature/Authorization/TeamAuthorizationTest.php +++ b/tests/Feature/Authorization/TeamAuthorizationTest.php @@ -126,6 +126,18 @@ expect(auth()->user()->can('update', $this->team))->toBeFalse(); }); +test('owner can update team MCP setting', function () { + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + + Livewire::test(TeamIndex::class) + ->set('is_mcp_server_enabled', false) + ->call('submit') + ->assertDispatched('success'); + + expect($this->team->fresh()->is_mcp_server_enabled)->toBeFalse(); +}); + // --- Team Index Livewire: delete --- test('member cannot delete team via index', function () { diff --git a/tests/Feature/Mcp/McpEndpointTest.php b/tests/Feature/Mcp/McpEndpointTest.php index b8511afef..f70584ecb 100644 --- a/tests/Feature/Mcp/McpEndpointTest.php +++ b/tests/Feature/Mcp/McpEndpointTest.php @@ -90,6 +90,16 @@ function expectMcpAuditLog(array $expected): void $response->assertStatus(404); }); +test('MCP endpoint returns 403 when the token team has MCP disabled', function () { + $this->team->update(['is_mcp_server_enabled' => false]); + $token = $this->user->createToken('mcp-read', ['read'])->plainTextToken; + + $response = mcpListTools($token); + + $response->assertForbidden(); + $response->assertJson(['message' => 'MCP server is disabled for this team.']); +}); + test('MCP endpoint rejects unauthenticated requests', function () { $response = mcpPost(['jsonrpc' => '2.0', 'id' => 1, 'method' => 'tools/list']); $response->assertStatus(401);