2026-04-29 08:30:43 +00:00
< ? php
namespace App\Mcp\Tools ;
use App\Mcp\Concerns\BuildsResponse ;
2026-08-03 21:08:11 +00:00
use App\Mcp\Concerns\McpStatusFilters ;
2026-04-29 08:30:43 +00:00
use App\Mcp\Concerns\ResolvesTeam ;
2026-08-03 21:08:11 +00:00
use App\Models\Application ;
use App\Models\ApplicationDeploymentQueue ;
use App\Models\Environment ;
2026-04-29 08:30:43 +00:00
use App\Models\Project ;
use App\Models\Server ;
2026-08-03 21:08:11 +00:00
use App\Models\Service ;
2026-04-29 08:30:43 +00:00
use Illuminate\Contracts\JsonSchema\JsonSchema ;
2026-08-03 21:08:11 +00:00
use Illuminate\Support\Collection ;
2026-04-29 08:30:43 +00:00
use Laravel\Mcp\Request ;
use Laravel\Mcp\Response ;
use Laravel\Mcp\Server\Tool ;
class GetInfrastructureOverview extends Tool
{
2026-06-04 09:03:06 +00:00
protected string $name = 'get_infrastructure_overview' ;
2026-08-03 21:08:11 +00:00
protected string $description = 'High-level overview of the authenticated team: Coolify version, servers, projects with resource counts, open deployments, and SQL-based health_hints counts. Start here or with coolify_help / search_resources.' ;
2026-06-04 09:03:06 +00:00
2026-04-29 08:30:43 +00:00
use BuildsResponse ;
2026-08-03 21:08:11 +00:00
use McpStatusFilters ;
2026-04-29 08:30:43 +00:00
use ResolvesTeam ;
public function handle ( Request $request ) : Response
{
2026-06-04 09:03:06 +00:00
if ( $error = $this -> ensureAbility ( $request , 'read' , $this -> name )) {
2026-04-29 08:30:43 +00:00
return $error ;
}
$teamId = $this -> resolveTeamId ( $request );
if ( is_null ( $teamId )) {
2026-06-04 09:03:06 +00:00
return $this -> mcpError ( $request , 'Invalid token.' );
2026-04-29 08:30:43 +00:00
}
$servers = Server :: whereTeamId ( $teamId )
-> select ( 'id' , 'name' , 'uuid' , 'ip' , 'description' )
-> with ( 'settings:id,server_id,is_reachable,is_usable' )
2026-08-03 21:08:11 +00:00
-> get ();
$serverSummaries = $servers
2026-04-29 08:30:43 +00:00
-> map ( fn ( $s ) => [
'uuid' => $s -> uuid ,
'name' => $s -> name ,
'ip' => $s -> ip ,
'is_reachable' => $s -> settings ? -> is_reachable ,
'is_usable' => $s -> settings ? -> is_usable ,
])
-> values ()
-> all ();
2026-08-03 21:08:11 +00:00
$unreachableServers = Server :: whereTeamId ( $teamId )
-> whereHas ( 'settings' , fn ( $q ) => $q -> where ( 'is_reachable' , false ))
-> count ();
// One query with relation counts (avoids per-project applications/services/databases fan-out).
$projects = Project :: where ( 'team_id' , $teamId )
-> select ( 'id' , 'uuid' , 'name' )
-> withCount ([
'applications' ,
'services' ,
'postgresqls' ,
'redis' ,
'mongodbs' ,
'mysqls' ,
'mariadbs' ,
'keydbs' ,
'dragonflies' ,
'clickhouses' ,
])
-> get ();
2026-04-29 08:30:43 +00:00
$appCount = 0 ;
$serviceCount = 0 ;
$databaseCount = 0 ;
$projectSummaries = [];
foreach ( $projects as $project ) {
2026-08-03 21:08:11 +00:00
$apps = ( int ) $project -> applications_count ;
$services = ( int ) $project -> services_count ;
$databases = ( int ) (
$project -> postgresqls_count
+ $project -> redis_count
+ $project -> mongodbs_count
+ $project -> mysqls_count
+ $project -> mariadbs_count
+ $project -> keydbs_count
+ $project -> dragonflies_count
+ $project -> clickhouses_count
);
2026-04-29 08:30:43 +00:00
$appCount += $apps ;
$serviceCount += $services ;
$databaseCount += $databases ;
$projectSummaries [] = [
'uuid' => $project -> uuid ,
'name' => $project -> name ,
'counts' => [
'applications' => $apps ,
'services' => $services ,
'databases' => $databases ,
],
];
}
2026-08-03 21:08:11 +00:00
// application_deployment_queues.application_id is varchar; whereHas joins to
// applications.id (bigint) and breaks on PostgreSQL. Scope via string IDs instead.
$teamApplicationIds = Application :: ownedByCurrentTeamAPI ( $teamId )
-> pluck ( 'id' )
-> map ( fn ( $id ) => ( string ) $id );
$openDeployments = ApplicationDeploymentQueue :: query ()
-> whereIn ( 'application_id' , $teamApplicationIds )
-> whereIn ( 'status' , [ 'in_progress' , 'queued' ])
-> count ();
// Count-only health hints (SQL for apps/DBs; chunked scan for service aggregated status).
$appNotRunningQuery = Application :: ownedByCurrentTeamAPI ( $teamId );
$this -> scopeNotHealthyRunning ( $appNotRunningQuery );
$nonRunningApps = $appNotRunningQuery -> count ();
$nonRunningServices = $this -> countUnhealthyServices ( $teamId );
$nonRunningDatabases = $this -> countUnhealthyDatabases ( $projects -> pluck ( 'id' ));
2026-06-04 09:03:06 +00:00
return $this -> mcpSuccess ( $request , $this -> respond ([
2026-04-29 08:30:43 +00:00
'coolify_version' => config ( 'constants.coolify.version' ),
2026-08-03 21:08:11 +00:00
'servers' => $serverSummaries ,
2026-04-29 08:30:43 +00:00
'projects' => $projectSummaries ,
'counts' => [
2026-08-03 21:08:11 +00:00
'servers' => count ( $serverSummaries ),
2026-04-29 08:30:43 +00:00
'projects' => count ( $projectSummaries ),
'applications' => $appCount ,
'services' => $serviceCount ,
'databases' => $databaseCount ,
2026-08-03 21:08:11 +00:00
'open_deployments' => $openDeployments ,
],
'health_hints' => [
'unreachable_servers' => $unreachableServers ,
'applications_not_running' => $nonRunningApps ,
'services_not_running' => $nonRunningServices ,
'databases_not_running' => $nonRunningDatabases ,
'next' => [
'tool' => 'list_unhealthy_resources' ,
'args' => [ 'sample_only' => true ],
'hint' => 'Sample unhealthy resources + full counts' ,
],
2026-04-29 08:30:43 +00:00
],
2026-06-04 09:03:06 +00:00
]));
2026-04-29 08:30:43 +00:00
}
2026-08-03 21:08:11 +00:00
/**
* Service status is a computed accessor over child apps / DBs — scan in chunks
* so large teams never hydrate every service at once for a count .
*/
private function countUnhealthyServices ( int $teamId ) : int
{
$count = 0 ;
Service :: whereHas ( 'environment.project' , fn ( $q ) => $q -> where ( 'team_id' , $teamId ))
-> with ([
'applications:id,service_id,status,exclude_from_status' ,
'databases:id,service_id,status,exclude_from_status' ,
])
-> select ([ 'id' , 'uuid' ])
-> orderBy ( 'id' )
-> chunkById ( 100 , function ( $chunk ) use ( & $count ) {
foreach ( $chunk as $service ) {
if ( ! $this -> looksHealthy ( $service -> status ? ? null )) {
$count ++ ;
}
}
});
return $count ;
}
/**
* One env - id fetch + one scoped count per standalone DB model ( constant query count ) .
*
* @ param Collection < int , int | string > $projectIds
*/
private function countUnhealthyDatabases ( Collection $projectIds ) : int
{
if ( $projectIds -> isEmpty ()) {
return 0 ;
}
$environmentIds = Environment :: query ()
-> whereIn ( 'project_id' , $projectIds )
-> pluck ( 'id' );
if ( $environmentIds -> isEmpty ()) {
return 0 ;
}
$count = 0 ;
foreach ( STANDALONE_DATABASE_MODELS as $modelClass ) {
$dbQuery = $modelClass :: query () -> whereIn ( 'environment_id' , $environmentIds );
$this -> scopeNotHealthyRunning ( $dbQuery );
$count += $dbQuery -> count ();
}
return $count ;
}
2026-04-29 08:30:43 +00:00
public function schema ( JsonSchema $schema ) : array
{
return [];
}
}