Add KanbanGit MCP v0.1
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Support;
|
||||
|
||||
use App\Actions\Permissions\PermissionService;
|
||||
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: string|null,
|
||||
* 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' => $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,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user