coolify/tests/Feature/Mcp/McpToggleApiTest.php
Andras Bacsai 7ab16ad7b5 feat(mcp): add MCP server with read-only tools for Coolify resources
Add Model Context Protocol server exposing Coolify infrastructure data
to AI assistants. Includes tools for listing/fetching servers, projects,
applications, databases, and services, scoped to authenticated team tokens.

- Add CoolifyServer with 10 read-only tools (list/get for all resource types)
- Add BuildsResponse and ResolvesTeam traits for shared tool logic
- Add EnsureMcpEnabled middleware guarding /mcp routes
- Add enable/disable MCP API endpoints (root-only)
- Add is_mcp_server_enabled toggle in instance settings and advanced UI
- Add migration for is_mcp_server_enabled column
- Add feature tests for MCP endpoints and toggle API
- Scrub sensitive keys (passwords, tokens, raw IDs) from all responses
2026-04-29 10:30:43 +02:00

107 lines
3.4 KiB
PHP

<?php
use App\Models\InstanceSettings;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::query()->delete();
$settings = new InstanceSettings([
'is_mcp_server_enabled' => false,
'is_api_enabled' => true,
]);
$settings->id = 0;
$settings->save();
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
session(['currentTeam' => $this->team]);
});
function makeRootMcpToken(User $user): string
{
$token = $user->createToken('mcp-root', ['root']);
DB::table('personal_access_tokens')
->where('id', $token->accessToken->id)
->update(['team_id' => '0']);
return $token->plainTextToken;
}
function makeNonRootMcpToken(User $user, Team $team, array $abilities = ['write']): string
{
$token = $user->createToken('mcp-write', $abilities);
DB::table('personal_access_tokens')
->where('id', $token->accessToken->id)
->update(['team_id' => (string) $team->id]);
return $token->plainTextToken;
}
test('GET /api/v1/mcp/enable enables MCP server with root token', function () {
$token = makeRootMcpToken($this->user);
$response = test()->withHeaders([
'Authorization' => 'Bearer '.$token,
])->getJson('/api/v1/mcp/enable');
$response->assertOk();
$response->assertJson(['message' => 'MCP server enabled.']);
expect(InstanceSettings::find(0)->is_mcp_server_enabled)->toBeTrue();
});
test('GET /api/v1/mcp/disable disables MCP server with root token', function () {
InstanceSettings::query()->where('id', 0)->update(['is_mcp_server_enabled' => true]);
$token = makeRootMcpToken($this->user);
$response = test()->withHeaders([
'Authorization' => 'Bearer '.$token,
])->getJson('/api/v1/mcp/disable');
$response->assertOk();
$response->assertJson(['message' => 'MCP server disabled.']);
expect(InstanceSettings::find(0)->is_mcp_server_enabled)->toBeFalse();
});
test('non-root token cannot enable MCP server', function () {
$token = makeNonRootMcpToken($this->user, $this->team);
$response = test()->withHeaders([
'Authorization' => 'Bearer '.$token,
])->getJson('/api/v1/mcp/enable');
$response->assertStatus(403);
expect(InstanceSettings::find(0)->is_mcp_server_enabled)->toBeFalse();
});
test('non-root token cannot disable MCP server', function () {
InstanceSettings::query()->where('id', 0)->update(['is_mcp_server_enabled' => true]);
$token = makeNonRootMcpToken($this->user, $this->team);
$response = test()->withHeaders([
'Authorization' => 'Bearer '.$token,
])->getJson('/api/v1/mcp/disable');
$response->assertStatus(403);
expect(InstanceSettings::find(0)->is_mcp_server_enabled)->toBeTrue();
});
test('unauthenticated request to /api/v1/mcp/enable returns 401', function () {
$response = test()->getJson('/api/v1/mcp/enable');
$response->assertStatus(401);
});
test('read-only token cannot toggle MCP server (lacks write ability)', function () {
$token = makeNonRootMcpToken($this->user, $this->team, ['read']);
$response = test()->withHeaders([
'Authorization' => 'Bearer '.$token,
])->getJson('/api/v1/mcp/enable');
$response->assertStatus(403);
});