diff --git a/.ai/lessons.md b/.ai/lessons.md index 20057d1493..79eb319551 100644 --- a/.ai/lessons.md +++ b/.ai/lessons.md @@ -11,6 +11,7 @@ - Files written on a server with `tee` get the server umask (027/077 on hardened hosts). Set the mode explicitly (`chmod 644`) for files that a non-root container user reads through a read-only bind mount. - `sshd` keeps the login shell of an open multiplexed SSH connection. Call `SshMultiplexingHelper::removeMuxFile()` before a live test of SSH or login-shell behavior. - SSH retries replay the full command batch. Only exit code 255 is an SSH transport failure; other exit codes come from the remote command, so do not retry them. Keep batches safe to run twice (for example `docker rm -f || true` before `docker run --name `). +- Do not put `pkill -f "playwright run-server"` in the same shell command as other work; the pattern matches that shell and kills it. Run it as its own command. - Redirect browser test output to a file (`> /tmp/x.log 2>&1`); piping it (`| tail`) hangs because the Playwright server keeps the pipe open. - Browser `click('Text')` can match a hidden element first (for example inactive modal steps) and wait forever; use `button:visible:has-text("Text")`. - Never run `php artisan test` inside a dev container (`./scripts/dev exec` or `docker exec coolify-dev-*`). The container sets `DB_CONNECTION=pgsql` as a real environment variable, which wins over `phpunit.xml`, so `RefreshDatabase` runs `migrate:fresh` on the dev database and wipes it. Run tests on the host only. diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index ca98698450..0a255370e5 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -2,7 +2,6 @@ namespace App\Livewire\Security; -use App\Models\InstanceSettings; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Validation\Rule; use Laravel\Sanctum\PersonalAccessToken; @@ -35,8 +34,6 @@ class ApiTokens extends Component 365 => '1 year', ]; - public $isApiEnabled; - #[Locked] public bool $canUseRootPermissions = false; @@ -56,7 +53,6 @@ class ApiTokens extends Component public function mount() { - $this->isApiEnabled = InstanceSettings::get()->is_api_enabled; $this->canUseRootPermissions = auth()->user()->can('useRootPermissions', PersonalAccessToken::class); $this->canUseWritePermissions = auth()->user()->can('useWritePermissions', PersonalAccessToken::class); $this->canUseDeployPermissions = auth()->user()->can('useDeployPermissions', PersonalAccessToken::class); diff --git a/resources/views/livewire/security/api-tokens.blade.php b/resources/views/livewire/security/api-tokens.blade.php index 8b05a0679b..2fd7a387c3 100644 --- a/resources/views/livewire/security/api-tokens.blade.php +++ b/resources/views/livewire/security/api-tokens.blade.php @@ -5,24 +5,6 @@ - @if (!$isApiEnabled) -
- - - @if (isInstanceAdmin()) - - - Enable API access - - - @endif - - -
- @else @php $expirationList = collect($expirationOptions) ->map(fn ($label, $days) => ['value' => (string) $days, 'label' => $label]) @@ -32,6 +14,9 @@ @endphp
+ + @can('create', App\Models\PersonalAccessToken::class)
@@ -262,6 +247,5 @@
- @endif
diff --git a/resources/views/livewire/settings/advanced.blade.php b/resources/views/livewire/settings/advanced.blade.php index 8f8961a2f4..dc18b41010 100644 --- a/resources/views/livewire/settings/advanced.blade.php +++ b/resources/views/livewire/settings/advanced.blade.php @@ -101,7 +101,12 @@ @endif @if ($is_mcp_server_enabled) - {{ url('/mcp') }} uses Sanctum bearer tokens from Security → API Tokens. +
+ {{ url('/mcp') }} + +
+ Authenticate with a bearer token from + Security → API Tokens.
@endif diff --git a/tests/Feature/ApiDisabledCalloutTest.php b/tests/Feature/ApiDisabledCalloutTest.php index 7f6ca8f6f8..4b2196e4ff 100644 --- a/tests/Feature/ApiDisabledCalloutTest.php +++ b/tests/Feature/ApiDisabledCalloutTest.php @@ -77,3 +77,17 @@ it('does not show the API notice on the webhooks page when the API is enabled', Livewire::test(Webhooks::class, ['resource' => $this->application]) ->assertDontSee('API access is turned off'); }); + +it('lets users create API tokens for MCP while the REST API is disabled', function () { + $this->actingAs($this->teamAdmin); + session(['currentTeam' => $this->team]); + + Livewire::test(ApiTokens::class) + ->assertSee('API access is turned off') + ->assertSee('Create token') + ->set('description', 'MCP client') + ->call('addNewToken') + ->assertHasNoErrors(); + + expect($this->teamAdmin->tokens()->where('name', 'MCP client')->exists())->toBeTrue(); +}); diff --git a/tests/Feature/SettingsAccessListboxTest.php b/tests/Feature/SettingsAccessListboxTest.php index 5d0f8c309b..8c251575f5 100644 --- a/tests/Feature/SettingsAccessListboxTest.php +++ b/tests/Feature/SettingsAccessListboxTest.php @@ -127,3 +127,24 @@ test('open API allowlist warning is shown when API access is enabled without all ->assertSet('is_api_enabled', true) ->assertSee('API access is open to every source'); }); + +test('MCP endpoint callout links to API token management', function () { + $rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]); + Server::factory()->create(['id' => 0, 'team_id' => $rootTeam->id]); + InstanceSettings::forceCreate([ + 'id' => 0, + 'is_api_enabled' => false, + 'is_mcp_server_enabled' => true, + ]); + Once::flush(); + + $user = User::factory()->create(); + $rootTeam->members()->attach($user->id, ['role' => 'admin']); + + $this->actingAs($user); + session(['currentTeam' => ['id' => $rootTeam->id]]); + + Livewire::test(Advanced::class) + ->assertSee(url('/mcp')) + ->assertSee('href="'.route('security.api-tokens').'"', false); +});