From 45f65481e637713a5c55f4e72461ad5fe879f513 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 5 May 2026 22:07:58 +0200 Subject: [PATCH] fix(mcp): change enable/disable endpoints from GET to POST and fix service/app listing - `/mcp/enable` and `/mcp/disable` now use POST (state-mutating ops) - `ListServices` queries DB directly instead of loading all projects into memory - `ListApplications` validates tag arg rejects empty string (not just falsy) --- app/Http/Controllers/Api/OtherController.php | 4 ++-- app/Mcp/Tools/ListApplications.php | 5 ++++- app/Mcp/Tools/ListServices.php | 18 ++++++++---------- openapi.json | 4 ++-- openapi.yaml | 4 ++-- routes/api.php | 4 ++-- tests/Feature/Mcp/McpToggleApiTest.php | 16 ++++++++-------- 7 files changed, 28 insertions(+), 27 deletions(-) diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php index 17c5a884a..f17a4e46b 100644 --- a/app/Http/Controllers/Api/OtherController.php +++ b/app/Http/Controllers/Api/OtherController.php @@ -153,7 +153,7 @@ public function disable_api(Request $request) return response()->json(['message' => 'API disabled.'], 200); } - #[OA\Get( + #[OA\Post( summary: 'Enable MCP Server', description: 'Enable the MCP server endpoint at /mcp (only with root permissions).', path: '/mcp/enable', @@ -209,7 +209,7 @@ public function enable_mcp(Request $request) return response()->json(['message' => 'MCP server enabled.'], 200); } - #[OA\Get( + #[OA\Post( summary: 'Disable MCP Server', description: 'Disable the MCP server endpoint at /mcp (only with root permissions).', path: '/mcp/disable', diff --git a/app/Mcp/Tools/ListApplications.php b/app/Mcp/Tools/ListApplications.php index 947d49ed1..815edd61a 100644 --- a/app/Mcp/Tools/ListApplications.php +++ b/app/Mcp/Tools/ListApplications.php @@ -31,10 +31,13 @@ public function handle(Request $request): Response } $tagName = $request->get('tag'); + if ($tagName !== null && (! is_string($tagName) || trim($tagName) === '')) { + return Response::error('tag argument must be a non-empty string.'); + } $args = $this->paginationArgs($request); $query = Application::ownedByCurrentTeamAPI($teamId) - ->when($tagName, function ($query, $tagName) { + ->when($tagName !== null, function ($query) use ($tagName) { $query->whereHas('tags', fn ($q) => $q->where('name', $tagName)); }); diff --git a/app/Mcp/Tools/ListServices.php b/app/Mcp/Tools/ListServices.php index 4b33231ba..b0bff4fad 100644 --- a/app/Mcp/Tools/ListServices.php +++ b/app/Mcp/Tools/ListServices.php @@ -4,7 +4,7 @@ use App\Mcp\Concerns\BuildsResponse; use App\Mcp\Concerns\ResolvesTeam; -use App\Models\Project; +use App\Models\Service; use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Mcp\Request; use Laravel\Mcp\Response; @@ -32,17 +32,15 @@ public function handle(Request $request): Response $args = $this->paginationArgs($request); - $projects = Project::where('team_id', $teamId)->get(); - $services = collect(); - foreach ($projects as $project) { - $services = $services->merge($project->services()->get()); - } + $query = Service::whereHas('environment.project', fn ($q) => $q->where('team_id', $teamId)); - $total = $services->count(); + $total = (clone $query)->count(); - $summaries = $services - ->sortBy('name') - ->slice($args['offset'], $args['per_page']) + $summaries = $query + ->orderBy('name') + ->skip($args['offset']) + ->take($args['per_page']) + ->get() ->map(fn ($svc) => [ 'uuid' => $svc->uuid, 'name' => $svc->name, diff --git a/openapi.json b/openapi.json index c35c0cdd6..711c7d8f3 100644 --- a/openapi.json +++ b/openapi.json @@ -8651,7 +8651,7 @@ } }, "\/mcp\/enable": { - "get": { + "post": { "summary": "Enable MCP Server", "description": "Enable the MCP server endpoint at \/mcp (only with root permissions).", "operationId": "enable-mcp", @@ -8703,7 +8703,7 @@ } }, "\/mcp\/disable": { - "get": { + "post": { "summary": "Disable MCP Server", "description": "Disable the MCP server endpoint at \/mcp (only with root permissions).", "operationId": "disable-mcp", diff --git a/openapi.yaml b/openapi.yaml index eab531674..fef77f5a7 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5485,7 +5485,7 @@ paths: - bearerAuth: [] /mcp/enable: - get: + post: summary: 'Enable MCP Server' description: 'Enable the MCP server endpoint at /mcp (only with root permissions).' operationId: enable-mcp @@ -5514,7 +5514,7 @@ paths: - bearerAuth: [] /mcp/disable: - get: + post: summary: 'Disable MCP Server' description: 'Disable the MCP server endpoint at /mcp (only with root permissions).' operationId: disable-mcp diff --git a/routes/api.php b/routes/api.php index f38576d4d..38ded350a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -35,8 +35,8 @@ ], function () { Route::get('/enable', [OtherController::class, 'enable_api']); Route::get('/disable', [OtherController::class, 'disable_api']); - Route::get('/mcp/enable', [OtherController::class, 'enable_mcp']); - Route::get('/mcp/disable', [OtherController::class, 'disable_mcp']); + Route::post('/mcp/enable', [OtherController::class, 'enable_mcp']); + Route::post('/mcp/disable', [OtherController::class, 'disable_mcp']); }); Route::group([ 'middleware' => ['auth:sanctum', ApiAllowed::class, 'api.sensitive'], diff --git a/tests/Feature/Mcp/McpToggleApiTest.php b/tests/Feature/Mcp/McpToggleApiTest.php index 2700f9b7b..68d5d335a 100644 --- a/tests/Feature/Mcp/McpToggleApiTest.php +++ b/tests/Feature/Mcp/McpToggleApiTest.php @@ -43,25 +43,25 @@ function makeNonRootMcpToken(User $user, Team $team, array $abilities = ['write' return $token->plainTextToken; } -test('GET /api/v1/mcp/enable enables MCP server with root token', function () { +test('POST /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'); + ])->postJson('/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 () { +test('POST /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'); + ])->postJson('/api/v1/mcp/disable'); $response->assertOk(); $response->assertJson(['message' => 'MCP server disabled.']); @@ -73,7 +73,7 @@ function makeNonRootMcpToken(User $user, Team $team, array $abilities = ['write' $response = test()->withHeaders([ 'Authorization' => 'Bearer '.$token, - ])->getJson('/api/v1/mcp/enable'); + ])->postJson('/api/v1/mcp/enable'); $response->assertStatus(403); expect(InstanceSettings::find(0)->is_mcp_server_enabled)->toBeFalse(); @@ -85,14 +85,14 @@ function makeNonRootMcpToken(User $user, Team $team, array $abilities = ['write' $response = test()->withHeaders([ 'Authorization' => 'Bearer '.$token, - ])->getJson('/api/v1/mcp/disable'); + ])->postJson('/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 = test()->postJson('/api/v1/mcp/enable'); $response->assertStatus(401); }); @@ -101,7 +101,7 @@ function makeNonRootMcpToken(User $user, Team $team, array $abilities = ['write' $response = test()->withHeaders([ 'Authorization' => 'Bearer '.$token, - ])->getJson('/api/v1/mcp/enable'); + ])->postJson('/api/v1/mcp/enable'); $response->assertStatus(403); });