76 lines
5.0 KiB
PHP
76 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace App\Mcp\Tools;
|
|
|
|
use App\Actions\Tickets\Support\TicketDescriptionFormat;
|
|
use App\Actions\Tickets\UpdateProjectTicket;
|
|
use App\Mcp\Support\DescribesTicketWriting;
|
|
use App\Mcp\Support\KanbanGitMcp;
|
|
use App\Mcp\Support\TicketUpdateInput;
|
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
|
use Illuminate\JsonSchema\Types\Type;
|
|
use Laravel\Mcp\Request;
|
|
use Laravel\Mcp\Response;
|
|
use Laravel\Mcp\ResponseFactory;
|
|
use Laravel\Mcp\Server\Attributes\Description;
|
|
use Laravel\Mcp\Server\Attributes\Name;
|
|
use Laravel\Mcp\Server\Tool;
|
|
use Laravel\Mcp\Server\Tools\Annotations\IsDestructive;
|
|
|
|
#[Name('update_ticket')]
|
|
#[Description('Partially update a ticket by ID using existing project permissions and ticket rules. Omitted fields stay unchanged. Explicit null clears nullable scalar fields only. acceptance_criteria and field_values replace their entire lists; [] clears a list and null is rejected. Checklist items with an existing ID retain their identity; omit id for a new item. Use move_ticket_status separately for workflow transitions. Author changes require project admin permission; people must be visible to the project.')]
|
|
#[IsDestructive(false)]
|
|
class UpdateTicket extends Tool
|
|
{
|
|
use DescribesTicketWriting;
|
|
|
|
public function __construct(
|
|
private readonly KanbanGitMcp $kanbanGitMcp,
|
|
private readonly TicketUpdateInput $ticketUpdateInput,
|
|
private readonly UpdateProjectTicket $updateProjectTicket,
|
|
) {}
|
|
|
|
public function handle(Request $request): Response|ResponseFactory
|
|
{
|
|
$selector = $request->validate(['ticket_id' => ['required', 'integer']]);
|
|
$user = $this->kanbanGitMcp->user($request);
|
|
$ticket = $this->kanbanGitMcp->ticketForWrite($user, (int) $selector['ticket_id']);
|
|
$validated = $this->ticketUpdateInput->validate($request, $ticket, $user);
|
|
$updated = $this->updateProjectTicket->handle($ticket, $user, $validated, true);
|
|
|
|
return Response::structured([
|
|
'data' => $this->kanbanGitMcp->ticketPayload($ticket->fresh()),
|
|
'meta' => ['updated' => $updated],
|
|
]);
|
|
}
|
|
|
|
/** @return array<string, Type> */
|
|
public function schema(JsonSchema $schema): array
|
|
{
|
|
return [
|
|
'ticket_id' => $schema->integer()->description('Existing ticket ID.')->required(),
|
|
'title' => $schema->string()->max(255)->description('Nonblank title; omit to preserve.'),
|
|
'description' => $schema->string()->nullable()->description('Description; null clears it. '.TicketDescriptionFormat::instructions()),
|
|
'ticket_type_id' => $schema->integer()->description('Type ID from get_project_configuration.'),
|
|
'ticket_priority_id' => $schema->integer()->description('Priority ID from get_project_configuration.'),
|
|
'parent_ticket_id' => $schema->integer()->nullable()->description('Parent-enabled ticket in this project; null removes the parent. Cycles are forbidden.'),
|
|
'assignee_user_id' => $schema->integer()->nullable()->description('Allowed person ID; null unassigns.'),
|
|
'author_user_id' => $schema->integer()->nullable()->description('Author ID; null clears. Changes require project admin permission.'),
|
|
'story_points' => $schema->integer()->min(0)->nullable()->description('Nonnegative estimate; null clears.'),
|
|
'is_blocked' => $schema->boolean()->description('Blocked state; true requires a nonblank blocker comment. Omit to preserve.'),
|
|
'blocked_comment' => $schema->string()->nullable()->description('Blocker reason; null clears only when the ticket is unblocked.'),
|
|
'acceptance_criteria' => $schema->array()->items($schema->object([
|
|
'id' => $schema->integer()->min(1)->description('Existing criterion ID belonging to this ticket. Omit for a new item. IDs must be unique.'),
|
|
'text' => $schema->string()->description('Nonblank criterion text.')->required(),
|
|
'checked' => $schema->boolean()->description('Checked state, defaults to false for supplied items.'),
|
|
'indent' => $schema->integer()->min(0)->max(8)->description('Indent level, defaults to 0; cannot skip a level.'),
|
|
])->withoutAdditionalProperties())->description('Ordered replacement checklist. Omitted items are removed. [] clears; omit field to preserve; null is invalid. '.TicketDescriptionFormat::criteriaInstructions()),
|
|
'field_values' => $schema->array()->items($schema->object([
|
|
'ticket_field_definition_id' => $schema->integer()->description('Project field ID, supplied once.')->required(),
|
|
'value_number' => $schema->number()->nullable()->description('Value for a numeric field; null clears.'),
|
|
'value_user_id' => $schema->integer()->nullable()->description('Allowed person ID for a user field; null clears.'),
|
|
])->withoutAdditionalProperties())->description('Replacement custom-field list. Omitted definitions are cleared. [] clears all; omit field to preserve; null is invalid.'),
|
|
];
|
|
}
|
|
}
|