OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
<?php
namespace Leantime\Domain\Projects\Tools;
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Laravel\Mcp\Server\Tools\ToolResult;
use Leantime\Domain\Modulemanager\Services\Modulemanager;
use Leantime\Domain\Projects\Services\Projects;
/**
* Creates a new project with the specified details.
*/
class AddProjectTool extends Tool
{
public function __construct(
private Projects $projectService,
private Modulemanager $moduleManager,
) {}
public function schema(ToolInputSchema $schema): ToolInputSchema
{
return $schema
->string('name')->description('Name of the project.')
->required()
->integer('clientId')->description('ID of the client for this project.')
->required()
->string('details')->description('Project description.')
->string('start')->description('Start date in ISO 8601 format.')
->string('end')->description('End date in ISO 8601 format.')
->integer('hourBudget')->description('Hour budget for the project.')
->integer('parent')->description('ID of the parent program or plan (only works if PgmPro plugin is active).');
}
public function name(): string
{
return 'addProject';
}
public function description(): string
{
return 'Creates a new project with the specified details.';
}
/**
* Handle the tool request.
*/
public function handle(array $arguments): ToolResult
{
$values = [
'name' => $arguments['name'],
'details' => ($arguments['details'] ?? ''),
'clientId' => (int) ($arguments['clientId'] ?? 0),
'hourBudget' => ($arguments['hourBudget'] ?? null),
'start' => ($arguments['start'] ?? null),
'end' => ($arguments['end'] ?? null),
];
// Check if PgmPro plugin is active and parent is specified
$parent = ($arguments['parent'] ?? null);
if ($parent && $this->moduleManager->isModuleAvailable('pgmPro')) {
$values['parent'] = $parent;
}
$projectId = $this->projectService->addProject($values);
if ($projectId) {
return ToolResult::text("Project created successfully with ID: $projectId");
}
return ToolResult::error('Failed to create project. Please check the provided information.');
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Leantime\Domain\Projects\Tools;
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Laravel\Mcp\Server\Tools\ToolResult;
use Leantime\Domain\Projects\Services\Projects;
/**
* 删除项目(高风险写操作)。
*
* 会删除项目本身 + 所有用户关系。调用方AI应先 getProject 列出项目信息
* 并征得用户明确确认。
*/
class DeleteProjectTool extends Tool
{
public function __construct(
private Projects $projectService,
) {}
public function name(): string
{
return 'deleteProject';
}
public function description(): string
{
return '删除一个项目(高风险操作,不可恢复)。会删除项目及其用户关联。调用前必须先 getProject 列出项目信息并征得用户明确确认。';
}
public function schema(ToolInputSchema $schema): ToolInputSchema
{
return $schema
->integer('id')->description('要删除的项目 ID。')->required()
->boolean('confirmed')->description('用户是否已明确确认删除。必须为 true 才执行。')->required();
}
public function handle(array $arguments): ToolResult
{
$id = (int) ($arguments['id'] ?? 0);
$confirmed = (bool) ($arguments['confirmed'] ?? false);
if (! $confirmed) {
return ToolResult::error('未确认删除。请先 getProject 列出项目信息,征得用户确认后再传 confirmed=true。');
}
$project = $this->projectService->getProject($id);
if (! isset($project['id'])) {
return ToolResult::error("项目不存在或无权访问:{$id}");
}
$name = $project['name'] ?? ('项目 #'.$id);
if ($this->projectService->deleteProject($id)) {
return ToolResult::text("项目已删除:{$name}ID {$id})。");
}
return ToolResult::error('删除失败(可能无权限或项目不存在)。');
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace Leantime\Domain\Projects\Tools;
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Laravel\Mcp\Server\Tools\ToolResult;
use Leantime\Domain\Modulemanager\Services\Modulemanager;
use Leantime\Domain\Projects\Services\Projects;
/**
* Updates an existing project with the specified details.
*/
class EditProjectTool extends Tool
{
public function __construct(
private Projects $projectService,
private Modulemanager $moduleManager,
) {}
public function schema(ToolInputSchema $schema): ToolInputSchema
{
return $schema
->integer('id')->description('ID of the project to update.')
->required()
->string('name')->description('Name of the project.')
->string('details')->description('Project description.')
->integer('clientId')->description('ID of the client for this project.')
->string('start')->description('Start date in ISO8601 format.')
->string('end')->description('End date in ISO8601 format.')
->integer('hourBudget')->description('Hour budget for the project.')
->integer('state')->description('Project state (0=open, 1=closed).')
->integer('parent')->description('ID of the parent program or plan (only works if PgmPro plugin is active).');
}
public function name(): string
{
return 'editProject';
}
public function description(): string
{
return 'Updates an existing project with the specified details.';
}
/**
* Handle the tool request.
*/
public function handle(array $arguments): ToolResult
{
$id = (int) ($arguments['id'] ?? 0);
// Get current project to ensure it exists
$currentProject = $this->projectService->getProject($id);
if (! $currentProject) {
return ToolResult::error('Project not found.');
}
$values = [];
// Only include parameters that were actually provided
$name = ($arguments['name'] ?? null);
if ($name !== null) {
$values['name'] = $name;
}
$details = ($arguments['details'] ?? null);
if ($details !== null) {
$values['details'] = $details;
}
$clientId = ($arguments['clientId'] ?? null);
if ($clientId !== null) {
$values['clientId'] = $clientId;
}
$start = ($arguments['start'] ?? null);
if ($start !== null) {
$values['start'] = $start;
}
$end = ($arguments['end'] ?? null);
if ($end !== null) {
$values['end'] = $end;
}
$hourBudget = ($arguments['hourBudget'] ?? null);
if ($hourBudget !== null) {
$values['hourBudget'] = $hourBudget;
}
$state = ($arguments['state'] ?? null);
if ($state !== null) {
$values['state'] = $state;
}
// Check if PgmPro plugin is active and parent is specified
$parent = ($arguments['parent'] ?? null);
if ($parent !== null && $this->moduleManager->isModuleAvailable('pgmPro')) {
$values['parent'] = $parent;
}
// If no values were provided, return early
if (empty($values)) {
return ToolResult::text('No changes provided for the project.');
}
$this->projectService->editProject($values, $id);
return ToolResult::text('Project updated successfully.');
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Leantime\Domain\Projects\Tools;
use Illuminate\Support\Str;
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Laravel\Mcp\Server\Tools\ToolResult;
use Leantime\Domain\Projects\Services\Projects;
/**
* Searches for projects by name.
*/
#[IsReadOnly]
class FindProjectTool extends Tool
{
public function __construct(
private Projects $projectService,
) {}
public function schema(ToolInputSchema $schema): ToolInputSchema
{
return $schema
->string('term')->description('Search term to find in project names.')
->required();
}
public function name(): string
{
return 'findProject';
}
public function description(): string
{
return 'Searches for projects by name.';
}
/**
* Handle the tool request.
*/
public function handle(array $arguments): ToolResult
{
$term = $arguments['term'];
$projects = $this->projectService->findProject($term);
if (empty($projects)) {
return ToolResult::text("No projects found matching: '$term'.");
}
$response = "## Projects Matching: '$term'\n";
foreach ($projects as $project) {
$result = [
'id' => $project['id'],
'name' => Str::sanitizeForLLM($project['name']),
'clientName' => Str::sanitizeForLLM($project['clientName']),
'type' => $project['type'],
'state' => $project['state'],
];
$response .= Str::toMarkdown($result)."\n";
}
return ToolResult::text($response);
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Leantime\Domain\Projects\Tools;
use Illuminate\Support\Str;
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Laravel\Mcp\Server\Tools\ToolResult;
use Leantime\Domain\Comments\Services\Comments;
use Leantime\Domain\Projects\Services\Projects;
/**
* Gets all projects the current user has access to with comprehensive progress information.
*/
#[IsReadOnly]
class GetAllProjectsTool extends Tool
{
public function __construct(
private Projects $projectService,
private Comments $commentsService,
) {}
public function schema(ToolInputSchema $schema): ToolInputSchema
{
return $schema
->boolean('showClosedProjects')->description('Whether to include closed projects in the results.')
->boolean('includeProgressDetails')->description('Whether to include detailed progress information (RAG status, completion dates, recent comments).');
}
public function name(): string
{
return 'getAllProjects';
}
public function description(): string
{
return 'Gets all projects the current user has access to with comprehensive progress information.';
}
/**
* Handle the tool request.
*/
public function handle(array $arguments): ToolResult
{
$showClosedProjects = $arguments['showClosedProjects'] ?? false;
$includeProgressDetails = $arguments['includeProgressDetails'] ?? true;
$projects = $this->projectService->getAll($showClosedProjects);
$response = "## All Projects Overview\n";
if (empty($projects)) {
return ToolResult::text('No projects found.');
}
foreach ($projects as $project) {
$result = [
'id' => $project['id'],
'name' => Str::sanitizeForLLM($project['name']),
'clientName' => Str::sanitizeForLLM($project['clientName']),
'type' => $project['type'],
'state' => $project['state'],
'start' => $project['start'] ?? 'Not set',
'end' => $project['end'] ?? 'Not set',
'progress' => isset($project['progress']['percent']) ? round($project['progress']['percent']).'%' : 'Not calculated',
];
// Add detailed progress information if requested
if ($includeProgressDetails) {
try {
$progress = $this->projectService->getProjectProgress($project['id']);
$projectComments = $this->commentsService->getComments('project', $project['id'], 1);
$result['progressPercent'] = isset($progress['percent']) ? round($progress['percent']).'%' : 'Not calculated';
$result['estimatedCompletion'] = isset($progress['estimatedCompletionDate']) ? strip_tags($progress['estimatedCompletionDate']) : 'Not set';
$result['plannedCompletion'] = $progress['plannedCompletionDate'] ?? 'Not set';
// Add RAG status and latest update
if (! empty($projectComments)) {
$latestComment = $projectComments[0];
$result['ragStatus'] = $this->formatRagStatus($latestComment['status'] ?? '');
$result['lastUpdate'] = [
'date' => $latestComment['date'],
'status' => $this->formatRagStatus($latestComment['status'] ?? ''),
'message' => Str::sanitizeForLLM($latestComment['comment'] ?? ''),
'author' => $latestComment['firstname'].' '.$latestComment['lastname'],
];
} else {
$result['ragStatus'] = 'Not set';
$result['lastUpdate'] = 'No updates available';
}
} catch (\Exception $e) {
// Fallback to basic progress info if detailed fetch fails
$result['ragStatus'] = 'Unable to fetch';
$result['lastUpdate'] = 'Unable to fetch';
}
}
$response .= Str::toMarkdown($result)."\n\n";
}
return ToolResult::text($response);
}
/**
* Format RAG status with appropriate emoji.
*/
private function formatRagStatus(string $status): string
{
return match (strtolower($status)) {
'green' => 'Green (On Track)',
'yellow' => 'Yellow (At Risk)',
'red' => 'Red (Critical)',
default => $status ?: 'Not Set'
};
}
}

View File

@@ -0,0 +1,226 @@
<?php
namespace Leantime\Domain\Projects\Tools;
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Laravel\Mcp\Server\Tools\ToolResult;
use Leantime\Domain\Comments\Services\Comments;
use Leantime\Domain\Projects\Services\Projects;
use Leantime\Domain\Tickets\Services\Tickets;
use Leantime\Domain\Timesheets\Services\Timesheets;
/**
* Gets comprehensive project information in a single call.
*/
#[IsReadOnly]
class GetFullProjectOverviewTool extends Tool
{
public function __construct(
private Projects $projectService,
private Comments $commentsService,
private Tickets $ticketsService,
) {}
public function schema(ToolInputSchema $schema): ToolInputSchema
{
return $schema
->integer('projectId')->description('Project ID to get full overview for.')
->required()
->boolean('includeTimesheets')->description('Whether to include timesheet data in the overview. Default false.')
->string('dateFrom')->description('Start date for timesheet data if included. ISO8601 format (example: 2024-04-30T15:00:00-04:00).')
->string('dateTo')->description('End date for timesheet data if included. ISO8601 format (example: 2024-04-30T15:00:00-04:00).');
}
public function name(): string
{
return 'getFullProjectOverview';
}
public function description(): string
{
return 'Gets comprehensive project information in a single call, combining project details, progress, comments, and optionally timesheets.';
}
/**
* Handle the tool request.
*/
public function handle(array $arguments): ToolResult
{
$projectId = (int) ($arguments['projectId'] ?? 0);
$includeTimesheets = $arguments['includeTimesheets'] ?? false;
$dateFrom = ($arguments['dateFrom'] ?? '');
$dateTo = ($arguments['dateTo'] ?? '');
// This method consolidates what would normally be 3-4 separate tool calls
$response = "# Complete Project Overview\n\n";
try {
// Get project details (equivalent to getProject tool)
$project = $this->projectService->getProject($projectId);
if (! $project) {
return ToolResult::error("Project with ID {$projectId} not found.");
}
$response .= "## Project Details\n";
$response .= "**Name:** {$project['name']}\n";
$response .= '**Client:** '.($project['clientName'] ?? 'No client')."\n";
$response .= "**Type:** {$project['type']}\n";
$response .= '**Status:** '.($project['state'] ?? 'Not set')."\n";
$response .= '**Start Date:** '.($project['start'] ?? 'Not set')."\n";
$response .= '**End Date:** '.($project['end'] ?? 'Not set')."\n";
$response .= "**Description:** {$project['details']}\n\n";
// Get project progress (equivalent to getProjectProgress tool)
$progress = $this->projectService->getProjectProgress($projectId);
$response .= "## Progress Overview\n";
$response .= '**Overall Progress:** '.($progress['percent'] ?? '0')."%\n";
$response .= '**RAG Status:** '.$this->formatRagStatus($progress['ragStatus'] ?? '')."\n";
$response .= '**Estimated Completion:** '.($progress['estimatedCompletionDate'] ?? 'Not calculated')."\n";
$response .= '**Planned Completion:** '.($progress['plannedCompletionDate'] ?? 'Not set')."\n\n";
// Get recent status updates/comments (equivalent to getAllProjectComments tool)
$comments = $this->commentsService->getComments('project', $projectId);
$response .= "## Recent Status Updates\n";
if (empty($comments)) {
$response .= "*No status updates found.*\n\n";
} else {
foreach ($comments as $comment) {
$status = $this->formatRagStatus($comment['status'] ?? '');
$response .= "**{$comment['date']}** - {$status}\n";
$response .= "*{$comment['firstname']} {$comment['lastname']}:* {$comment['text']}\n\n";
}
}
// Optionally include timesheet data
if ($includeTimesheets) {
$response .= "## Time Tracking Summary\n";
// Set default date range if not provided
if (empty($dateFrom)) {
$dateFrom = date('Y-m-01'); // First day of current month
}
if (empty($dateTo)) {
$dateTo = date('Y-m-d'); // Today
}
try {
$timesheets = app(Timesheets::class)->getAll(
dateFrom: dtHelper()->parseUserDateTime($dateFrom)->startOfDay(),
dateTo: dtHelper()->parseUserDateTime($dateTo)->endOfDay(),
projectId: $projectId
);
if (empty($timesheets)) {
$response .= "*No timesheet entries found for the specified period.*\n\n";
} else {
$totalHours = 0;
$userHours = [];
foreach ($timesheets as $entry) {
$hours = floatval($entry['hours'] ?? 0);
$totalHours += $hours;
$user = $entry['firstname'].' '.$entry['lastname'];
$userHours[$user] = ($userHours[$user] ?? 0) + $hours;
}
$response .= "**Total Hours Logged:** {$totalHours}h\n";
$response .= "**Period:** {$dateFrom} to {$dateTo}\n";
$response .= "**Team Breakdown:**\n";
foreach ($userHours as $user => $hours) {
$response .= "- {$user}: {$hours}h\n";
}
$response .= "\n";
}
} catch (\Exception $e) {
$response .= "*Could not retrieve timesheet data.*\n\n";
}
}
// Add quick task summary
$response .= "## Task Summary\n";
try {
$allTasks = $this->ticketsService->getAll(['currentProject' => $projectId], 100);
$taskStats = $this->calculateTaskStats($allTasks);
$response .= "**Total Tasks:** {$taskStats['total']}\n";
$response .= "**Completed:** {$taskStats['completed']} ({$taskStats['completedPercent']}%)\n";
$response .= "**In Progress:** {$taskStats['inProgress']}\n";
$response .= "**Not Started:** {$taskStats['notStarted']}\n";
$response .= "**Overdue:** {$taskStats['overdue']}\n\n";
} catch (\Exception $e) {
$response .= "*Could not retrieve task statistics.*\n\n";
}
return ToolResult::text($response);
} catch (\Exception $e) {
return ToolResult::error('Error retrieving project overview: '.$e->getMessage());
}
}
/**
* Calculate task statistics from a list of tasks.
*
* @param array $tasks Array of task data.
* @return array<string, int|float> Computed statistics.
*/
private function calculateTaskStats(array $tasks): array
{
$stats = [
'total' => count($tasks),
'completed' => 0,
'inProgress' => 0,
'notStarted' => 0,
'overdue' => 0,
'completedPercent' => 0,
];
$now = new \DateTime;
foreach ($tasks as $task) {
$status = $task['status'] ?? '';
$dueDate = $task['dateToFinish'] ?? '';
// Count by status type
if (in_array($status, ['done', 'closed', 'completed'])) {
$stats['completed']++;
} elseif (in_array($status, ['inprogress', 'working', 'development'])) {
$stats['inProgress']++;
} else {
$stats['notStarted']++;
}
// Check for overdue tasks
if (! empty($dueDate) && ! in_array($status, ['done', 'closed', 'completed'])) {
$due = new \DateTime($dueDate);
if ($due < $now) {
$stats['overdue']++;
}
}
}
if ($stats['total'] > 0) {
$stats['completedPercent'] = round(($stats['completed'] / $stats['total']) * 100, 1);
}
return $stats;
}
/**
* Format RAG status with appropriate label.
*/
private function formatRagStatus(string $status): string
{
return match (strtolower($status)) {
'green' => 'Green (On Track)',
'yellow' => 'Yellow (At Risk)',
'red' => 'Red (Critical)',
default => $status ?: 'Not Set'
};
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Leantime\Domain\Projects\Tools;
use Illuminate\Support\Str;
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Laravel\Mcp\Server\Tools\ToolResult;
use Leantime\Domain\Comments\Services\Comments;
use Leantime\Domain\Projects\Services\Projects;
/**
* Gets detailed information about a specific project by its ID.
*/
#[IsReadOnly]
class GetProjectTool extends Tool
{
public function __construct(
private Projects $projectService,
private Comments $commentsService,
) {}
public function schema(ToolInputSchema $schema): ToolInputSchema
{
return $schema
->integer('projectId')->description('ID of the project to retrieve.')
->required();
}
public function name(): string
{
return 'getProject';
}
public function description(): string
{
return 'Gets detailed information about a specific project by its ID and project progress.';
}
/**
* Handle the tool request.
*/
public function handle(array $arguments): ToolResult
{
$projectId = (int) ($arguments['projectId'] ?? 0);
$project = $this->projectService->getProject($projectId);
if (! $project) {
return ToolResult::error('Project not found.');
}
$progress = $this->projectService->getProjectProgress($projectId);
$projectComment = $this->commentsService->getComments('project', $project['id']);
$project['team'] = $this->projectService->getUsersAssignedToProject($project['id']);
if (is_array($projectComment) && count($projectComment) > 0) {
$project['lastUpdate'] = $projectComment[0];
$project['status'] = $projectComment[0]['status'];
} else {
$project['lastUpdate'] = false;
$project['status'] = '';
}
$project['progress'] = $progress;
$response = "## Project Details\n";
$result = [
'id' => $project['id'],
'name' => Str::sanitizeForLLM($project['name']),
'details' => Str::sanitizeForLLM($project['details']),
'clientName' => Str::sanitizeForLLM($project['clientName'] ?? ''),
'type' => $project['type'],
'state' => $project['state'],
'ragStatus' => $project['status'],
'lastUpdateMessage' => $project['lastUpdate'],
'start' => $project['start'] ?? 'Not set',
'end' => $project['end'] ?? 'Not set',
'progress' => isset($progress['percent']) ? round($progress['percent']).'%' : 'Not calculated',
'estimatedCompletionDate' => isset($progress['estimatedCompletionDate']) ? strip_tags($progress['estimatedCompletionDate']) : 'Unknown',
];
$response .= Str::toMarkdown($result)."\n";
return ToolResult::text($response);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Leantime\Domain\Projects\Tools;
use Illuminate\Support\Str;
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Laravel\Mcp\Server\Tools\ToolResult;
use Leantime\Domain\Projects\Services\Projects;
/**
* Gets all projects assigned to a specific user.
*/
#[IsReadOnly]
class GetProjectsAssignedToUserTool extends Tool
{
public function __construct(
private Projects $projectService,
) {}
public function schema(ToolInputSchema $schema): ToolInputSchema
{
return $schema
->integer('userId')->description('User ID to get projects for.')
->required()
->string('projectStatus')->description('Filter by project status (open, closed, all).')
->integer('clientId')->description('Filter by client ID.')
->string('projectTypes')->description('Filter by project types (comma-separated: project, program, etc.). Use "all" for all project types.');
}
public function name(): string
{
return 'getProjectsAssignedToUser';
}
public function description(): string
{
return 'Gets all projects assigned to a specific user.';
}
/**
* Handle the tool request.
*/
public function handle(array $arguments): ToolResult
{
$userId = (int) ($arguments['userId'] ?? 0);
$projectStatus = ($arguments['projectStatus'] ?? 'open');
$clientId = ($arguments['clientId'] ?? null);
$projectTypes = ($arguments['projectTypes'] ?? 'all');
if ($userId === 0) {
$userId = session('userdata.id') ?? '';
}
$projects = $this->projectService->getProjectsAssignedToUser($userId, $projectStatus, $clientId, $projectTypes);
$response = "## Projects Assigned to User\n";
foreach ($projects as $project) {
$result = [
'id' => $project['id'],
'name' => Str::sanitizeForLLM($project['name']),
'clientName' => Str::sanitizeForLLM($project['clientName']),
'type' => $project['type'],
'state' => $project['state'],
'start' => $project['start'] ?? 'Not set',
'end' => $project['end'] ?? 'Not set',
];
$response .= Str::toMarkdown($result)."\n";
}
if (empty($projects)) {
return ToolResult::text('No projects found.');
}
return ToolResult::text($response);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Leantime\Domain\Projects\Tools;
use Illuminate\Support\Str;
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Laravel\Mcp\Server\Tools\ToolResult;
use Leantime\Domain\Projects\Services\Projects;
/**
* Gets all users assigned to a specific project.
*/
#[IsReadOnly]
class GetUsersAssignedToProjectTool extends Tool
{
public function __construct(
private Projects $projectService,
) {}
public function schema(ToolInputSchema $schema): ToolInputSchema
{
return $schema
->integer('projectId')->description('ID of the project to get users for.')
->required()
->boolean('teamOnly')->description('Whether to only include direct team members.');
}
public function name(): string
{
return 'getUsersAssignedToProject';
}
public function description(): string
{
return 'Gets all users assigned to a specific project.';
}
/**
* Handle the tool request.
*/
public function handle(array $arguments): ToolResult
{
$projectId = (int) ($arguments['projectId'] ?? 0);
$teamOnly = $arguments['teamOnly'] ?? false;
$users = $this->projectService->getUsersAssignedToProject($projectId, $teamOnly);
if (empty($users)) {
return ToolResult::text('No users assigned to this project.');
}
$response = "## Users Assigned to Project\n";
foreach ($users as $user) {
$result = [
'id' => $user['id'],
'name' => Str::sanitizeForLLM($user['firstname'].' '.$user['lastname']),
'email' => $user['username'],
'role' => $user['role'] ?? 'Not specified',
'projectRole' => $user['projectRole'] ?? 'Not specified',
];
$response .= Str::toMarkdown($result)."\n";
}
return ToolResult::text($response);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Leantime\Domain\Projects\Tools;
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Laravel\Mcp\Server\Tools\ToolResult;
use Leantime\Domain\Modulemanager\Services\Modulemanager;
use Leantime\Domain\Projects\Services\Projects;
/**
* Updates specific fields of an existing project.
*/
class PatchProjectTool extends Tool
{
public function __construct(
private Projects $projectService,
private Modulemanager $moduleManager,
) {}
public function schema(ToolInputSchema $schema): ToolInputSchema
{
return $schema
->integer('id')->description('ID of the project to update.')
->required()
->raw('params', ['type' => 'object', 'description' => 'Key-value pairs of fields to update. Example: {"name": "New name", "state": 0}'])->required();
}
public function name(): string
{
return 'patchProject';
}
public function description(): string
{
return 'Updates specific fields of an existing project.';
}
/**
* Handle the tool request.
*/
public function handle(array $arguments): ToolResult
{
$id = (int) ($arguments['id'] ?? 0);
$params = ($arguments['params'] ?? null);
// Get current project to ensure it exists
$currentProject = $this->projectService->getProject($id);
if (! $currentProject) {
return ToolResult::error('Project not found.');
}
// Check if $params is array of arrays (AI sometimes does this)
if (is_array($params) && ! empty($params) && isset($params[0]) && is_array($params[0])) {
$params = $params[0];
}
if (! is_array($params)) {
return ToolResult::error('The params parameter is not a valid object. Please provide an object of key-value pairs.');
}
// Handle parent field if PgmPro plugin is active
if (isset($params['parent']) && ! $this->moduleManager->isModuleAvailable('pgmPro')) {
unset($params['parent']);
}
$result = $this->projectService->patch($id, $params);
if ($result) {
return ToolResult::text('Project updated successfully.');
}
return ToolResult::error('Failed to update project. Please check the provided information.');
}
}