Files
KanbanGit/app/Mcp/Support/KanbanGitMcp.php
T
Koda 3ddfcc89f9
linter / quality (pull_request) Successful in 1m16s
tests / ci (8.4) (pull_request) Successful in 1m33s
Add checklist acceptance criteria
2026-05-18 23:24:52 +00:00

247 lines
7.3 KiB
PHP

<?php
namespace App\Mcp\Support;
use App\Actions\Permissions\PermissionService;
use App\Actions\Tickets\Support\TicketAcceptanceCriteriaData;
use App\Enums\Permission;
use App\Http\Resources\ProjectResource;
use App\Http\Resources\TicketResource;
use App\Models\Project;
use App\Models\Ticket;
use App\Models\TicketStatus;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
class KanbanGitMcp
{
public function __construct(
private readonly PermissionService $permissionService,
) {}
/**
* @throws AuthenticationException
*/
public function user(Request $request): User
{
$user = $request->user();
if (! $user instanceof User) {
throw new AuthenticationException('Authentication required.');
}
return $user;
}
/**
* @throws AuthorizationException
*/
public function ensureCanRead(User $user, Project $project): void
{
if (! $this->permissionService->allows($user, Permission::ProjectRead, $project)) {
throw new AuthorizationException('You cannot view this project.');
}
}
/**
* @throws AuthorizationException
*/
public function ensureCanWrite(User $user, Project $project): void
{
if (! $this->permissionService->allows($user, Permission::ProjectWrite, $project)) {
throw new AuthorizationException('You cannot update tickets in this project.');
}
}
/**
* @throws AuthorizationException
*/
public function ticketForRead(User $user, int $ticketId): Ticket
{
$ticket = Ticket::query()
->with('project')
->find($ticketId);
if ($ticket === null) {
throw ValidationException::withMessages([
'ticket_id' => 'Ticket was not found.',
]);
}
$this->ensureCanRead($user, $ticket->project);
return $ticket;
}
/**
* @throws AuthorizationException
*/
public function ticketForWrite(User $user, int $ticketId): Ticket
{
$ticket = Ticket::query()
->with('project')
->find($ticketId);
if ($ticket === null) {
throw ValidationException::withMessages([
'ticket_id' => 'Ticket was not found.',
]);
}
$this->ensureCanWrite($user, $ticket->project);
return $ticket;
}
/**
* @throws AuthorizationException
*/
public function projectForRead(User $user, ?int $projectId): ?Project
{
if ($projectId === null) {
return null;
}
$project = Project::query()->find($projectId);
if ($project === null) {
throw ValidationException::withMessages([
'project_id' => 'Project was not found.',
]);
}
$this->ensureCanRead($user, $project);
return $project;
}
/**
* @param array<string, mixed> $validated
* @return Collection<int, TicketStatus>|Response
*/
public function resolveTicketStatuses(User $user, array $validated, ?Project $project = null): Collection|Response
{
$projectIds = $project === null
? $this->permissionService->viewableProjectIds($user)
: [$project->id];
if ($projectIds === []) {
return collect();
}
$ticketStatusId = $validated['ticket_status_id'] ?? null;
if ($ticketStatusId !== null) {
$ticketStatus = TicketStatus::query()
->whereIn('project_id', $projectIds)
->with('boardColumn')
->find((int) $ticketStatusId);
return $ticketStatus === null
? Response::error('Ticket status was not found in a visible project.')
: collect([$ticketStatus]);
}
$query = TicketStatus::query()
->whereIn('project_id', $projectIds)
->with(['boardColumn', 'project'])
->orderBy('project_id')
->orderBy('position');
if (($statusSlug = $validated['status_slug'] ?? null) !== null) {
$query->where('slug', $statusSlug);
} elseif (($statusName = $validated['status_name'] ?? null) !== null) {
$query->where('name', $statusName);
} else {
return Response::error('Provide ticket_status_id, status_slug, or status_name.');
}
$ticketStatuses = $query->get();
if ($ticketStatuses->count() <= 1) {
return $ticketStatuses;
}
$matches = $ticketStatuses
->map(fn (TicketStatus $ticketStatus): array => [
'id' => $ticketStatus->id,
'name' => $ticketStatus->name,
'project_id' => $ticketStatus->project_id,
'project_name' => $ticketStatus->project?->name,
'slug' => $ticketStatus->slug,
])
->values()
->all();
return Response::error(sprintf(
'Status lookup is ambiguous. Provide ticket_status_id or project_id. Matches: %s',
json_encode($matches, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES),
));
}
/**
* @return array<string, mixed>
*/
public function ticketPayload(Ticket $ticket): array
{
$ticket->load([
'assignee',
'author',
'fieldValues.ticketFieldDefinition',
'fieldValues.userValue',
'project',
'ticketPriority',
'ticketStatus.boardColumn',
'ticketType',
'worklogEntries.actor',
]);
return [
'project' => ProjectResource::make($ticket->project)->resolve(),
'ticket' => TicketResource::make($ticket)->resolve(),
];
}
/**
* @return array{
* acceptance_criteria: array{
* summary: string|null,
* items: array<int, array{checked: bool, indent: int, text: string}>
* },
* assignee_user_id: int|null,
* author_user_id: int|null,
* blocked_comment: string|null,
* description: string|null,
* is_blocked: bool,
* parent_ticket_id: int|null,
* story_points: int|null,
* ticket_priority_id: int,
* ticket_type_id: int,
* title: string
* }
*/
public function updatePayload(Ticket $ticket): array
{
return [
'acceptance_criteria' => TicketAcceptanceCriteriaData::fromStored(
$ticket->acceptance_criteria_data,
$ticket->acceptance_criteria,
),
'assignee_user_id' => $ticket->assignee_user_id,
'author_user_id' => $ticket->author_user_id,
'blocked_comment' => $ticket->blocked_comment,
'description' => $ticket->description,
'is_blocked' => $ticket->is_blocked,
'parent_ticket_id' => $ticket->parent_ticket_id,
'story_points' => $ticket->story_points,
'ticket_priority_id' => $ticket->ticket_priority_id,
'ticket_type_id' => $ticket->ticket_type_id,
'title' => $ticket->title,
];
}
}