Licenses MCP stuff
This commit is contained in:
@@ -6,23 +6,30 @@ use App\Mcp\Tools\AuditAssetTool;
|
||||
use App\Mcp\Tools\CheckinAccessoryTool;
|
||||
use App\Mcp\Tools\CheckinAssetTool;
|
||||
use App\Mcp\Tools\CheckinComponentTool;
|
||||
use App\Mcp\Tools\CheckinLicenseTool;
|
||||
use App\Mcp\Tools\CheckoutAccessoryTool;
|
||||
use App\Mcp\Tools\CheckoutAssetTool;
|
||||
use App\Mcp\Tools\CheckoutComponentTool;
|
||||
use App\Mcp\Tools\CheckoutLicenseTool;
|
||||
use App\Mcp\Tools\CreateAccessoryTool;
|
||||
use App\Mcp\Tools\CreateComponentTool;
|
||||
use App\Mcp\Tools\CreateLicenseTool;
|
||||
use App\Mcp\Tools\CreateUserTool;
|
||||
use App\Mcp\Tools\DeleteAccessoryTool;
|
||||
use App\Mcp\Tools\DeleteAssetTool;
|
||||
use App\Mcp\Tools\DeleteComponentTool;
|
||||
use App\Mcp\Tools\DeleteLicenseTool;
|
||||
use App\Mcp\Tools\DeleteUserTool;
|
||||
use App\Mcp\Tools\ListAssetsTool;
|
||||
use App\Mcp\Tools\ListLicensesTool;
|
||||
use App\Mcp\Tools\ListUsersTool;
|
||||
use App\Mcp\Tools\ShowAssetTool;
|
||||
use App\Mcp\Tools\ShowLicenseTool;
|
||||
use App\Mcp\Tools\ShowUserTool;
|
||||
use App\Mcp\Tools\UpdateAccessoryTool;
|
||||
use App\Mcp\Tools\UpdateAssetTool;
|
||||
use App\Mcp\Tools\UpdateComponentTool;
|
||||
use App\Mcp\Tools\UpdateLicenseTool;
|
||||
use App\Mcp\Tools\UpdateUserTool;
|
||||
use Laravel\Mcp\Server;
|
||||
use Laravel\Mcp\Server\Attributes\Instructions;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
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\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkin_license')]
|
||||
#[Title('Checkin License')]
|
||||
#[Description('Check in a Snipe-IT license seat by its seat ID, returning it to the available pool')]
|
||||
class CheckinLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'seat_id' => 'required|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$seat = LicenseSeat::with('license')->find($request->get('seat_id'));
|
||||
|
||||
if (! $seat) {
|
||||
return Response::make(Response::error('License seat not found'));
|
||||
}
|
||||
|
||||
if (is_null($seat->assigned_to) && is_null($seat->asset_id)) {
|
||||
return Response::make(Response::error('This seat is not currently checked out'));
|
||||
}
|
||||
|
||||
$license = $seat->license;
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error('License not found'));
|
||||
}
|
||||
|
||||
// License checkin uses the checkout gate (matching application behavior)
|
||||
if (! Gate::allows('checkout', $license)) {
|
||||
return Response::make(Response::error('Unauthorized'));
|
||||
}
|
||||
|
||||
$returnTo = null;
|
||||
if ($seat->assigned_to) {
|
||||
$returnTo = User::withTrashed()->find($seat->assigned_to);
|
||||
} elseif ($seat->asset_id) {
|
||||
$returnTo = Asset::find($seat->asset_id);
|
||||
}
|
||||
|
||||
$note = $request->get('note');
|
||||
|
||||
$seat->assigned_to = null;
|
||||
$seat->asset_id = null;
|
||||
$seat->notes = $note;
|
||||
|
||||
if (! $license->reassignable) {
|
||||
$seat->unreassignable_seat = true;
|
||||
}
|
||||
|
||||
if ($seat->save()) {
|
||||
event(new CheckoutableCheckedIn($seat, $returnTo, auth()->user(), $note));
|
||||
|
||||
return Response::make(
|
||||
Response::text('License seat '.$seat->id.' checked in successfully')
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => 'License seat checked in successfully',
|
||||
'seat_id' => $seat->id,
|
||||
'license_id' => $license->id,
|
||||
'license_name' => $license->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error('Checkin failed'));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'seat_id' => $schema->number()->description('ID of the license seat to check in (returned by checkout_license)'),
|
||||
'note' => $schema->string()->description('Optional checkin note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkin succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'seat_id' => $schema->number()->description('ID of the seat that was checked in'),
|
||||
'license_id' => $schema->number()->description('Numeric ID of the license'),
|
||||
'license_name' => $schema->string()->description('Name of the license'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Models\Asset;
|
||||
use App\Models\License;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
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\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_license')]
|
||||
#[Title('Checkout License')]
|
||||
#[Description('Check out an available license seat to a user or asset')]
|
||||
class CheckoutLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'assigned_to' => 'nullable|integer',
|
||||
'asset_id' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$license = $this->resolveLicense($request);
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error('License not found'));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $license)) {
|
||||
return Response::make(Response::error('Unauthorized'));
|
||||
}
|
||||
|
||||
if ($license->numRemaining() < 1) {
|
||||
return Response::make(Response::error('No available seats for this license'));
|
||||
}
|
||||
|
||||
if (! $request->filled('assigned_to') && ! $request->filled('asset_id')) {
|
||||
return Response::make(Response::error('Please provide either assigned_to (user ID) or asset_id'));
|
||||
}
|
||||
|
||||
$seat = $license->freeSeat();
|
||||
|
||||
if (! $seat) {
|
||||
return Response::make(Response::error('No free seat found for this license'));
|
||||
}
|
||||
|
||||
$note = $request->get('note');
|
||||
|
||||
if ($request->filled('assigned_to')) {
|
||||
$target = User::find($request->get('assigned_to'));
|
||||
if (! $target) {
|
||||
return Response::make(Response::error('User not found'));
|
||||
}
|
||||
$seat->assigned_to = $target->id;
|
||||
$seat->notes = $note;
|
||||
|
||||
if ($seat->save()) {
|
||||
event(new CheckoutableCheckedOut($seat, $target, auth()->user(), $note, [], 1));
|
||||
|
||||
return Response::make(
|
||||
Response::text('License seat checked out to user '.$target->username)
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => 'License seat checked out successfully',
|
||||
'license_id' => $license->id,
|
||||
'license_name' => $license->name,
|
||||
'seat_id' => $seat->id,
|
||||
'assigned_to_type' => 'user',
|
||||
'assigned_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
} elseif ($request->filled('asset_id')) {
|
||||
$target = Asset::find($request->get('asset_id'));
|
||||
if (! $target) {
|
||||
return Response::make(Response::error('Asset not found'));
|
||||
}
|
||||
$seat->asset_id = $target->id;
|
||||
if ($target->checkedOutToUser()) {
|
||||
$seat->assigned_to = $target->assigned_to;
|
||||
}
|
||||
$seat->notes = $note;
|
||||
|
||||
if ($seat->save()) {
|
||||
event(new CheckoutableCheckedOut($seat, $target, auth()->user(), $note, [], 1));
|
||||
|
||||
return Response::make(
|
||||
Response::text('License seat checked out to asset '.$target->asset_tag)
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => 'License seat checked out successfully',
|
||||
'license_id' => $license->id,
|
||||
'license_name' => $license->name,
|
||||
'seat_id' => $seat->id,
|
||||
'assigned_to_type' => 'asset',
|
||||
'assigned_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return Response::make(Response::error('Checkout failed'));
|
||||
}
|
||||
|
||||
private function resolveLicense(Request $request): ?License
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return License::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return License::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the license to check out'),
|
||||
'name' => $schema->string()->description('Name of the license to check out'),
|
||||
'assigned_to' => $schema->number()->description('User ID to assign the seat to'),
|
||||
'asset_id' => $schema->number()->description('Asset ID to assign the seat to'),
|
||||
'note' => $schema->string()->description('Optional checkout note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkout succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'license_id' => $schema->number()->description('Numeric ID of the license'),
|
||||
'license_name' => $schema->string()->description('Name of the license'),
|
||||
'seat_id' => $schema->number()->description('ID of the seat record (use this for checkin)'),
|
||||
'assigned_to_type' => $schema->string()->description('Type of entity checked out to: user or asset'),
|
||||
'assigned_to_id' => $schema->number()->description('ID of the entity checked out to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\License;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
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\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_license')]
|
||||
#[Title('Create License')]
|
||||
#[Description('Create a new Snipe-IT software license')]
|
||||
class CreateLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', License::class)) {
|
||||
return Response::make(Response::error('Unauthorized'));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'seats' => 'required|integer|min:1',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'purchase_date' => 'nullable|date_format:Y-m-d',
|
||||
'purchase_cost' => 'nullable|numeric|min:0',
|
||||
'purchase_order' => 'nullable|string|max:255',
|
||||
'order_number' => 'nullable|string|max:255',
|
||||
'expiration_date' => 'nullable|date_format:Y-m-d',
|
||||
'termination_date' => 'nullable|date_format:Y-m-d',
|
||||
'license_name' => 'nullable|string|max:255',
|
||||
'license_email' => 'nullable|email|max:255',
|
||||
'maintained' => 'nullable|boolean',
|
||||
'reassignable' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
'min_amt' => 'nullable|integer|min:0',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$license = new License;
|
||||
$license->fill($request->only([
|
||||
'name', 'seats', 'category_id', 'serial', 'manufacturer_id',
|
||||
'supplier_id', 'purchase_date', 'purchase_cost', 'purchase_order',
|
||||
'order_number', 'expiration_date', 'termination_date',
|
||||
'license_name', 'license_email', 'maintained', 'reassignable',
|
||||
'notes', 'min_amt',
|
||||
]));
|
||||
|
||||
$license->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$license->created_by = auth()->id();
|
||||
|
||||
if ($license->save()) {
|
||||
return Response::make(
|
||||
Response::text('License '.$license->name.' created successfully')
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => 'License created successfully',
|
||||
'id' => $license->id,
|
||||
'name' => $license->name,
|
||||
'seats' => $license->seats,
|
||||
'category_id' => $license->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error('Create failed: '.$license->getErrors()->first()));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('License name (required)'),
|
||||
'seats' => $schema->number()->description('Number of seats (required, min 1)'),
|
||||
'category_id' => $schema->number()->description('Category ID — must be a license category (required)'),
|
||||
'serial' => $schema->string()->description('Product key / serial number'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'purchase_cost' => $schema->number()->description('Purchase cost'),
|
||||
'purchase_order' => $schema->string()->description('Purchase order number'),
|
||||
'order_number' => $schema->string()->description('Order number'),
|
||||
'expiration_date' => $schema->string()->description('License expiration date (YYYY-MM-DD)'),
|
||||
'termination_date' => $schema->string()->description('License termination date (YYYY-MM-DD)'),
|
||||
'license_name' => $schema->string()->description('Name of the licensed user/organization'),
|
||||
'license_email' => $schema->string()->description('Email of the licensed user/organization'),
|
||||
'maintained' => $schema->boolean()->description('Whether the license is under maintenance'),
|
||||
'reassignable' => $schema->boolean()->description('Whether seats can be reassigned after checkin'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'min_amt' => $schema->number()->description('Minimum seat threshold for alerts'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the license was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new license'),
|
||||
'name' => $schema->string()->description('Name of the new license'),
|
||||
'seats' => $schema->number()->description('Total seat count'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\License;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
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\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_license')]
|
||||
#[Title('Delete License')]
|
||||
#[Description('Soft-delete a Snipe-IT license. The license must have no seats currently assigned to users or assets.')]
|
||||
class DeleteLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$license = $this->resolveLicense($request);
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error('License not found'));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $license)) {
|
||||
return Response::make(Response::error('Unauthorized'));
|
||||
}
|
||||
|
||||
if ($license->assignedCount()->count() > 0) {
|
||||
return Response::make(Response::error('License has seats currently assigned and cannot be deleted. Check in all seats first.'));
|
||||
}
|
||||
|
||||
$name = $license->name;
|
||||
|
||||
DB::table('license_seats')
|
||||
->where('license_id', $license->id)
|
||||
->update(['assigned_to' => null, 'asset_id' => null]);
|
||||
|
||||
$license->licenseseats()->delete();
|
||||
$license->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text('License '.$name.' deleted successfully')
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => 'License deleted successfully',
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveLicense(Request $request): ?License
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return License::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return License::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the license to delete'),
|
||||
'name' => $schema->string()->description('Name of the license to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted license'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\License;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
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\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_licenses')]
|
||||
#[Title('List Licenses')]
|
||||
#[Description('Search and list Snipe-IT licenses with optional filtering by keyword, company, category, and manufacturer')]
|
||||
class ListLicensesTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('index', License::class)) {
|
||||
return Response::make(Response::error('Unauthorized'));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'company_id' => 'nullable|integer',
|
||||
'category_id' => 'nullable|integer',
|
||||
'manufacturer_id' => 'nullable|integer',
|
||||
'supplier_id' => 'nullable|integer',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$licenses = License::with('company', 'manufacturer', 'supplier', 'category')
|
||||
->withCount('freeSeats as free_seats_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$licenses->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$licenses->where('licenses.company_id', '=', $request->get('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$licenses->where('category_id', '=', $request->get('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('manufacturer_id')) {
|
||||
$licenses->where('manufacturer_id', '=', $request->get('manufacturer_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('supplier_id')) {
|
||||
$licenses->where('supplier_id', '=', $request->get('supplier_id'));
|
||||
}
|
||||
|
||||
$licenses->orderBy('licenses.created_at', 'desc');
|
||||
|
||||
$total = $licenses->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $licenses->skip($offset)->take($limit)->get();
|
||||
|
||||
$licensesData = $results->map(fn (License $license) => [
|
||||
'id' => $license->id,
|
||||
'name' => $license->name,
|
||||
'serial' => $license->serial,
|
||||
'seats' => $license->seats,
|
||||
'free_seats' => $license->free_seats_count,
|
||||
'category' => $license->category?->name,
|
||||
'manufacturer' => $license->manufacturer?->name,
|
||||
'company' => $license->company?->name,
|
||||
'supplier' => $license->supplier?->name,
|
||||
'expiration_date' => $license->expiration_date?->format('Y-m-d'),
|
||||
'purchase_date' => $license->purchase_date?->format('Y-m-d'),
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text("Found {$total} licenses, returning ".count($licensesData))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'licenses' => $licensesData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across name, serial, notes, and order number'),
|
||||
'company_id' => $schema->number()->description('Filter by company ID'),
|
||||
'category_id' => $schema->number()->description('Filter by category ID'),
|
||||
'manufacturer_id' => $schema->number()->description('Filter by manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Filter by supplier ID'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching licenses')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\License;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
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\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('show_license')]
|
||||
#[Title('Show License Details')]
|
||||
#[Description('Look up a single Snipe-IT license by numeric ID or name and return its full details including seat counts')]
|
||||
class ShowLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
if ($request->filled('id')) {
|
||||
$license = License::with('company', 'manufacturer', 'supplier', 'category')
|
||||
->withCount('freeSeats as free_seats_count')
|
||||
->find($request->get('id'));
|
||||
} elseif ($request->filled('name')) {
|
||||
$license = License::with('company', 'manufacturer', 'supplier', 'category')
|
||||
->withCount('freeSeats as free_seats_count')
|
||||
->where('name', $request->get('name'))
|
||||
->first();
|
||||
} else {
|
||||
return Response::make(Response::error('Please provide an id or name'));
|
||||
}
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error('License not found'));
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', $license)) {
|
||||
return Response::make(Response::error('Unauthorized'));
|
||||
}
|
||||
|
||||
$assignedCount = $license->assignedCount()->count();
|
||||
|
||||
return Response::make(
|
||||
Response::text('License '.$license->name.' found')
|
||||
)->withStructuredContent([
|
||||
'id' => $license->id,
|
||||
'name' => $license->name,
|
||||
'serial' => $license->serial,
|
||||
'seats' => $license->seats,
|
||||
'free_seats' => $license->free_seats_count,
|
||||
'assigned_seats' => $assignedCount,
|
||||
'category' => $license->category?->name,
|
||||
'category_id' => $license->category_id,
|
||||
'manufacturer' => $license->manufacturer?->name,
|
||||
'manufacturer_id' => $license->manufacturer_id,
|
||||
'company' => $license->company?->name,
|
||||
'company_id' => $license->company_id,
|
||||
'supplier' => $license->supplier?->name,
|
||||
'supplier_id' => $license->supplier_id,
|
||||
'license_name' => $license->license_name,
|
||||
'license_email' => $license->license_email,
|
||||
'maintained' => (bool) $license->maintained,
|
||||
'reassignable' => (bool) $license->reassignable,
|
||||
'purchase_date' => $license->purchase_date?->format('Y-m-d'),
|
||||
'purchase_cost' => $license->purchase_cost,
|
||||
'purchase_order' => $license->purchase_order,
|
||||
'order_number' => $license->order_number,
|
||||
'expiration_date' => $license->expiration_date?->format('Y-m-d'),
|
||||
'termination_date' => $license->termination_date?->format('Y-m-d'),
|
||||
'notes' => $license->notes,
|
||||
'created_at' => $license->created_at?->format('Y-m-d H:i:s'),
|
||||
'updated_at' => $license->updated_at?->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric license ID'),
|
||||
'name' => $schema->string()->description('License name to look up'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric license ID')->required(),
|
||||
'name' => $schema->string()->description('License name')->required(),
|
||||
'seats' => $schema->number()->description('Total seat count'),
|
||||
'free_seats' => $schema->number()->description('Number of available (unassigned) seats'),
|
||||
'assigned_seats' => $schema->number()->description('Number of currently assigned seats'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\License;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
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\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('update_license')]
|
||||
#[Title('Update License')]
|
||||
#[Description('Update fields on a Snipe-IT license identified by numeric ID or name')]
|
||||
class UpdateLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'new_name' => 'nullable|string|max:255',
|
||||
'seats' => 'nullable|integer|min:1',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'category_id' => 'nullable|integer|exists:categories,id',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'purchase_date' => 'nullable|date_format:Y-m-d',
|
||||
'purchase_cost' => 'nullable|numeric|min:0',
|
||||
'purchase_order' => 'nullable|string|max:255',
|
||||
'order_number' => 'nullable|string|max:255',
|
||||
'expiration_date' => 'nullable|date_format:Y-m-d',
|
||||
'termination_date' => 'nullable|date_format:Y-m-d',
|
||||
'license_name' => 'nullable|string|max:255',
|
||||
'license_email' => 'nullable|email|max:255',
|
||||
'maintained' => 'nullable|boolean',
|
||||
'reassignable' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
'min_amt' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$license = $this->resolveLicense($request);
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error('License not found'));
|
||||
}
|
||||
|
||||
if (! Gate::allows('update', $license)) {
|
||||
return Response::make(Response::error('Unauthorized'));
|
||||
}
|
||||
|
||||
$updatable = [
|
||||
'seats', 'serial', 'category_id', 'manufacturer_id', 'supplier_id',
|
||||
'purchase_date', 'purchase_cost', 'purchase_order', 'order_number',
|
||||
'expiration_date', 'termination_date', 'license_name', 'license_email',
|
||||
'maintained', 'reassignable', 'notes', 'min_amt',
|
||||
];
|
||||
|
||||
foreach ($updatable as $field) {
|
||||
if ($request->filled($field)) {
|
||||
$license->{$field} = $request->get($field);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->filled('new_name')) {
|
||||
$license->name = $request->get('new_name');
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$license->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
}
|
||||
|
||||
if ($license->save()) {
|
||||
return Response::make(
|
||||
Response::text('License '.$license->name.' updated successfully')
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => 'License updated successfully',
|
||||
'id' => $license->id,
|
||||
'name' => $license->name,
|
||||
'seats' => $license->seats,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error('Update failed: '.$license->getErrors()->first()));
|
||||
}
|
||||
|
||||
private function resolveLicense(Request $request): ?License
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return License::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return License::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID to identify the license'),
|
||||
'name' => $schema->string()->description('Name to identify the license'),
|
||||
'new_name' => $schema->string()->description('New name (renames the license)'),
|
||||
'seats' => $schema->number()->description('Total seat count'),
|
||||
'serial' => $schema->string()->description('Product key / serial number'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'company_id' => $schema->number()->description('Company ID'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'purchase_cost' => $schema->number()->description('Purchase cost'),
|
||||
'purchase_order' => $schema->string()->description('Purchase order number'),
|
||||
'order_number' => $schema->string()->description('Order number'),
|
||||
'expiration_date' => $schema->string()->description('Expiration date (YYYY-MM-DD)'),
|
||||
'termination_date' => $schema->string()->description('Termination date (YYYY-MM-DD)'),
|
||||
'license_name' => $schema->string()->description('Name of the licensed user/organization'),
|
||||
'license_email' => $schema->string()->description('Email of the licensed user/organization'),
|
||||
'maintained' => $schema->boolean()->description('Whether the license is under maintenance'),
|
||||
'reassignable' => $schema->boolean()->description('Whether seats can be reassigned after checkin'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'min_amt' => $schema->number()->description('Minimum seat threshold for alerts'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the update succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the license'),
|
||||
'name' => $schema->string()->description('Name of the license'),
|
||||
'seats' => $schema->number()->description('Total seat count'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Mcp;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Mcp\Tools\CheckinLicenseTool;
|
||||
use App\Mcp\Tools\CheckoutLicenseTool;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CheckinLicenseToolTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->actingAs(User::factory()->checkoutLicenses()->create());
|
||||
}
|
||||
|
||||
private function handle(array $args): ResponseFactory
|
||||
{
|
||||
return (new CheckinLicenseTool)->handle(new Request($args));
|
||||
}
|
||||
|
||||
private function checkoutToUser(License $license, User $user): LicenseSeat
|
||||
{
|
||||
$response = (new CheckoutLicenseTool)->handle(new Request([
|
||||
'id' => $license->id,
|
||||
'assigned_to' => $user->id,
|
||||
]));
|
||||
|
||||
$seatId = $response->getStructuredContent()['seat_id'];
|
||||
|
||||
return LicenseSeat::find($seatId);
|
||||
}
|
||||
|
||||
public function test_checks_in_seat_by_seat_id()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
|
||||
$user = User::factory()->create();
|
||||
$seat = $this->checkoutToUser($license, $user);
|
||||
|
||||
$content = $this->handle(['seat_id' => $seat->id])->getStructuredContent();
|
||||
|
||||
$this->assertTrue($content['success']);
|
||||
$this->assertDatabaseHas('license_seats', [
|
||||
'id' => $seat->id,
|
||||
'assigned_to' => null,
|
||||
'asset_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_response_includes_license_info()
|
||||
{
|
||||
$license = License::factory()->create(['name' => 'Checkin License', 'seats' => 3, 'reassignable' => true]);
|
||||
$user = User::factory()->create();
|
||||
$seat = $this->checkoutToUser($license, $user);
|
||||
|
||||
$content = $this->handle(['seat_id' => $seat->id])->getStructuredContent();
|
||||
|
||||
$this->assertEquals($seat->id, $content['seat_id']);
|
||||
$this->assertEquals($license->id, $content['license_id']);
|
||||
$this->assertEquals('Checkin License', $content['license_name']);
|
||||
}
|
||||
|
||||
public function test_fires_checkin_event()
|
||||
{
|
||||
Event::fake([CheckoutableCheckedIn::class]);
|
||||
|
||||
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
|
||||
$user = User::factory()->create();
|
||||
$seat = $this->checkoutToUser($license, $user);
|
||||
|
||||
$this->handle(['seat_id' => $seat->id]);
|
||||
|
||||
Event::assertDispatched(CheckoutableCheckedIn::class);
|
||||
}
|
||||
|
||||
public function test_returns_error_when_seat_not_found()
|
||||
{
|
||||
$this->assertTrue($this->handle(['seat_id' => 999999])->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_returns_error_when_seat_is_not_checked_out()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
|
||||
$seat = $license->freeSeat();
|
||||
|
||||
$this->assertTrue($this->handle(['seat_id' => $seat->id])->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_sets_unreassignable_flag_when_license_not_reassignable()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 1, 'reassignable' => false]);
|
||||
$user = User::factory()->create();
|
||||
|
||||
$seat = $license->freeSeat();
|
||||
$seat->assigned_to = $user->id;
|
||||
$seat->save();
|
||||
|
||||
$this->handle(['seat_id' => $seat->id]);
|
||||
|
||||
$this->assertDatabaseHas('license_seats', [
|
||||
'id' => $seat->id,
|
||||
'unreassignable_seat' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_does_not_set_unreassignable_flag_when_license_is_reassignable()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
|
||||
$user = User::factory()->create();
|
||||
$seat = $this->checkoutToUser($license, $user);
|
||||
|
||||
$this->handle(['seat_id' => $seat->id]);
|
||||
|
||||
$refreshed = LicenseSeat::find($seat->id);
|
||||
$this->assertFalse((bool) $refreshed->unreassignable_seat);
|
||||
}
|
||||
|
||||
public function test_returns_error_when_user_lacks_permission()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
|
||||
$user = User::factory()->create();
|
||||
$seat = $this->checkoutToUser($license, $user);
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$this->assertTrue($this->handle(['seat_id' => $seat->id])->responses()->first()->isError());
|
||||
$this->assertDatabaseHas('license_seats', [
|
||||
'id' => $seat->id,
|
||||
'assigned_to' => $user->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Mcp;
|
||||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Mcp\Tools\CheckoutLicenseTool;
|
||||
use App\Models\Asset;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CheckoutLicenseToolTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->actingAs(User::factory()->checkoutLicenses()->create());
|
||||
}
|
||||
|
||||
private function handle(array $args): ResponseFactory
|
||||
{
|
||||
return (new CheckoutLicenseTool)->handle(new Request($args));
|
||||
}
|
||||
|
||||
public function test_checks_out_license_to_user_by_id()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
|
||||
$user = User::factory()->create();
|
||||
|
||||
$content = $this->handle([
|
||||
'id' => $license->id,
|
||||
'assigned_to' => $user->id,
|
||||
])->getStructuredContent();
|
||||
|
||||
$this->assertTrue($content['success']);
|
||||
$this->assertEquals('user', $content['assigned_to_type']);
|
||||
$this->assertEquals($user->id, $content['assigned_to_id']);
|
||||
$this->assertDatabaseHas('license_seats', [
|
||||
'license_id' => $license->id,
|
||||
'assigned_to' => $user->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_checks_out_license_to_user_by_name()
|
||||
{
|
||||
$license = License::factory()->create(['name' => 'Named Checkout License', 'seats' => 3, 'reassignable' => true]);
|
||||
$user = User::factory()->create();
|
||||
|
||||
$content = $this->handle([
|
||||
'name' => 'Named Checkout License',
|
||||
'assigned_to' => $user->id,
|
||||
])->getStructuredContent();
|
||||
|
||||
$this->assertTrue($content['success']);
|
||||
}
|
||||
|
||||
public function test_checks_out_license_to_asset()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
|
||||
$asset = Asset::factory()->create();
|
||||
|
||||
$content = $this->handle([
|
||||
'id' => $license->id,
|
||||
'asset_id' => $asset->id,
|
||||
])->getStructuredContent();
|
||||
|
||||
$this->assertTrue($content['success']);
|
||||
$this->assertEquals('asset', $content['assigned_to_type']);
|
||||
$this->assertEquals($asset->id, $content['assigned_to_id']);
|
||||
$this->assertDatabaseHas('license_seats', [
|
||||
'license_id' => $license->id,
|
||||
'asset_id' => $asset->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_response_includes_seat_id_for_checkin()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
|
||||
$user = User::factory()->create();
|
||||
|
||||
$content = $this->handle([
|
||||
'id' => $license->id,
|
||||
'assigned_to' => $user->id,
|
||||
])->getStructuredContent();
|
||||
|
||||
$this->assertArrayHasKey('seat_id', $content);
|
||||
$this->assertIsInt($content['seat_id']);
|
||||
$this->assertNotNull(LicenseSeat::find($content['seat_id']));
|
||||
}
|
||||
|
||||
public function test_fires_checkout_event()
|
||||
{
|
||||
Event::fake([CheckoutableCheckedOut::class]);
|
||||
|
||||
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->handle([
|
||||
'id' => $license->id,
|
||||
'assigned_to' => $user->id,
|
||||
]);
|
||||
|
||||
Event::assertDispatched(CheckoutableCheckedOut::class);
|
||||
}
|
||||
|
||||
public function test_returns_error_when_no_seats_remaining()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 1, 'reassignable' => true]);
|
||||
$user1 = User::factory()->create();
|
||||
$user2 = User::factory()->create();
|
||||
|
||||
$seat = $license->freeSeat();
|
||||
$seat->assigned_to = $user1->id;
|
||||
$seat->save();
|
||||
|
||||
$response = $this->handle([
|
||||
'id' => $license->id,
|
||||
'assigned_to' => $user2->id,
|
||||
]);
|
||||
|
||||
$this->assertTrue($response->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_returns_error_when_neither_assignee_provided()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 3]);
|
||||
|
||||
$this->assertTrue($this->handle(['id' => $license->id])->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_returns_error_when_license_not_found()
|
||||
{
|
||||
$this->assertTrue($this->handle([
|
||||
'id' => 999999,
|
||||
'assigned_to' => User::factory()->create()->id,
|
||||
])->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_returns_error_when_user_not_found()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 3]);
|
||||
|
||||
$this->assertTrue($this->handle([
|
||||
'id' => $license->id,
|
||||
'assigned_to' => 999999,
|
||||
])->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_returns_error_when_user_lacks_permission()
|
||||
{
|
||||
$this->actingAs(User::factory()->create());
|
||||
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->assertTrue($this->handle([
|
||||
'id' => $license->id,
|
||||
'assigned_to' => $user->id,
|
||||
])->responses()->first()->isError());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Mcp;
|
||||
|
||||
use App\Mcp\Tools\CreateLicenseTool;
|
||||
use App\Models\Category;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\User;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CreateLicenseToolTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->actingAs(User::factory()->createLicenses()->create());
|
||||
}
|
||||
|
||||
private function handle(array $args): ResponseFactory
|
||||
{
|
||||
return (new CreateLicenseTool)->handle(new Request($args));
|
||||
}
|
||||
|
||||
public function test_creates_license_with_required_fields()
|
||||
{
|
||||
$category = Category::factory()->create(['category_type' => 'license']);
|
||||
|
||||
$content = $this->handle([
|
||||
'name' => 'Test License',
|
||||
'seats' => 5,
|
||||
'category_id' => $category->id,
|
||||
])->getStructuredContent();
|
||||
|
||||
$this->assertTrue($content['success']);
|
||||
$this->assertDatabaseHas('licenses', ['name' => 'Test License', 'seats' => 5]);
|
||||
}
|
||||
|
||||
public function test_response_includes_id_name_seats_and_category()
|
||||
{
|
||||
$category = Category::factory()->create(['category_type' => 'license']);
|
||||
|
||||
$content = $this->handle([
|
||||
'name' => 'Response License',
|
||||
'seats' => 10,
|
||||
'category_id' => $category->id,
|
||||
])->getStructuredContent();
|
||||
|
||||
$this->assertArrayHasKey('id', $content);
|
||||
$this->assertEquals('Response License', $content['name']);
|
||||
$this->assertEquals(10, $content['seats']);
|
||||
$this->assertEquals($category->id, $content['category_id']);
|
||||
}
|
||||
|
||||
public function test_creates_license_seats_automatically()
|
||||
{
|
||||
$category = Category::factory()->create(['category_type' => 'license']);
|
||||
|
||||
$content = $this->handle([
|
||||
'name' => 'Seat Auto License',
|
||||
'seats' => 3,
|
||||
'category_id' => $category->id,
|
||||
])->getStructuredContent();
|
||||
|
||||
$licenseId = $content['id'];
|
||||
$this->assertEquals(3, LicenseSeat::where('license_id', $licenseId)->count());
|
||||
}
|
||||
|
||||
public function test_creates_license_with_optional_fields()
|
||||
{
|
||||
$category = Category::factory()->create(['category_type' => 'license']);
|
||||
|
||||
$this->handle([
|
||||
'name' => 'Full License',
|
||||
'seats' => 2,
|
||||
'category_id' => $category->id,
|
||||
'serial' => 'SN-FULL-001',
|
||||
'license_name' => 'Acme Corp',
|
||||
'license_email' => 'admin@acme.com',
|
||||
'purchase_date' => '2024-01-15',
|
||||
'purchase_cost' => 299.99,
|
||||
'expiration_date' => '2025-01-15',
|
||||
'maintained' => true,
|
||||
'reassignable' => false,
|
||||
'notes' => 'Important license',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('licenses', [
|
||||
'name' => 'Full License',
|
||||
'serial' => 'SN-FULL-001',
|
||||
'license_name' => 'Acme Corp',
|
||||
'license_email' => 'admin@acme.com',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_returns_error_when_name_missing()
|
||||
{
|
||||
$category = Category::factory()->create(['category_type' => 'license']);
|
||||
|
||||
$this->assertTrue($this->handle([
|
||||
'seats' => 5,
|
||||
'category_id' => $category->id,
|
||||
])->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_returns_error_when_seats_missing()
|
||||
{
|
||||
$category = Category::factory()->create(['category_type' => 'license']);
|
||||
|
||||
$this->assertTrue($this->handle([
|
||||
'name' => 'No Seats',
|
||||
'category_id' => $category->id,
|
||||
])->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_returns_error_when_category_missing()
|
||||
{
|
||||
$this->assertTrue($this->handle([
|
||||
'name' => 'No Category',
|
||||
'seats' => 5,
|
||||
])->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_returns_error_when_category_does_not_exist()
|
||||
{
|
||||
$this->assertTrue($this->handle([
|
||||
'name' => 'Bad Category',
|
||||
'seats' => 5,
|
||||
'category_id' => 999999,
|
||||
])->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_returns_error_when_user_lacks_permission()
|
||||
{
|
||||
$this->actingAs(User::factory()->create());
|
||||
$category = Category::factory()->create(['category_type' => 'license']);
|
||||
|
||||
$this->assertTrue($this->handle([
|
||||
'name' => 'Blocked License',
|
||||
'seats' => 5,
|
||||
'category_id' => $category->id,
|
||||
])->responses()->first()->isError());
|
||||
|
||||
$this->assertDatabaseMissing('licenses', ['name' => 'Blocked License']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Mcp;
|
||||
|
||||
use App\Mcp\Tools\DeleteLicenseTool;
|
||||
use App\Models\License;
|
||||
use App\Models\User;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DeleteLicenseToolTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->actingAs(User::factory()->deleteLicenses()->create());
|
||||
}
|
||||
|
||||
private function handle(array $args): ResponseFactory
|
||||
{
|
||||
return (new DeleteLicenseTool)->handle(new Request($args));
|
||||
}
|
||||
|
||||
public function test_deletes_license_by_id()
|
||||
{
|
||||
$license = License::factory()->create();
|
||||
|
||||
$content = $this->handle(['id' => $license->id])->getStructuredContent();
|
||||
|
||||
$this->assertTrue($content['success']);
|
||||
$this->assertSoftDeleted('licenses', ['id' => $license->id]);
|
||||
}
|
||||
|
||||
public function test_deletes_license_by_name()
|
||||
{
|
||||
$license = License::factory()->create(['name' => 'Delete By Name License']);
|
||||
|
||||
$content = $this->handle(['name' => 'Delete By Name License'])->getStructuredContent();
|
||||
|
||||
$this->assertTrue($content['success']);
|
||||
$this->assertSoftDeleted('licenses', ['id' => $license->id]);
|
||||
}
|
||||
|
||||
public function test_response_includes_name()
|
||||
{
|
||||
$license = License::factory()->create(['name' => 'Named License']);
|
||||
|
||||
$content = $this->handle(['id' => $license->id])->getStructuredContent();
|
||||
|
||||
$this->assertEquals('Named License', $content['name']);
|
||||
}
|
||||
|
||||
public function test_returns_error_when_not_found()
|
||||
{
|
||||
$this->assertTrue($this->handle(['id' => 999999])->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_returns_error_when_seats_are_assigned()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
|
||||
$user = User::factory()->create();
|
||||
|
||||
$seat = $license->freeSeat();
|
||||
$seat->assigned_to = $user->id;
|
||||
$seat->save();
|
||||
|
||||
$response = $this->handle(['id' => $license->id]);
|
||||
|
||||
$this->assertTrue($response->responses()->first()->isError());
|
||||
$this->assertNotSoftDeleted('licenses', ['id' => $license->id]);
|
||||
}
|
||||
|
||||
public function test_returns_error_when_user_lacks_permission()
|
||||
{
|
||||
$license = License::factory()->create();
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$this->assertTrue($this->handle(['id' => $license->id])->responses()->first()->isError());
|
||||
$this->assertNotSoftDeleted('licenses', ['id' => $license->id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Mcp;
|
||||
|
||||
use App\Mcp\Tools\ListLicensesTool;
|
||||
use App\Models\Category;
|
||||
use App\Models\License;
|
||||
use App\Models\Manufacturer;
|
||||
use App\Models\User;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ListLicensesToolTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->actingAs(User::factory()->viewLicenses()->create());
|
||||
}
|
||||
|
||||
private function handle(array $args = []): ResponseFactory
|
||||
{
|
||||
return (new ListLicensesTool)->handle(new Request($args));
|
||||
}
|
||||
|
||||
public function test_returns_total_licenses_and_pagination_metadata()
|
||||
{
|
||||
License::factory()->count(3)->create();
|
||||
|
||||
$content = $this->handle()->getStructuredContent();
|
||||
|
||||
$this->assertArrayHasKey('total', $content);
|
||||
$this->assertArrayHasKey('licenses', $content);
|
||||
$this->assertArrayHasKey('limit', $content);
|
||||
$this->assertArrayHasKey('offset', $content);
|
||||
$this->assertGreaterThanOrEqual(3, $content['total']);
|
||||
}
|
||||
|
||||
public function test_each_license_includes_key_fields()
|
||||
{
|
||||
License::factory()->create();
|
||||
|
||||
$content = $this->handle()->getStructuredContent();
|
||||
|
||||
$license = $content['licenses'][0];
|
||||
$this->assertArrayHasKey('id', $license);
|
||||
$this->assertArrayHasKey('name', $license);
|
||||
$this->assertArrayHasKey('seats', $license);
|
||||
$this->assertArrayHasKey('free_seats', $license);
|
||||
$this->assertArrayHasKey('category', $license);
|
||||
}
|
||||
|
||||
public function test_limit_controls_number_of_results_returned()
|
||||
{
|
||||
License::factory()->count(10)->create();
|
||||
|
||||
$content = $this->handle(['limit' => 3])->getStructuredContent();
|
||||
|
||||
$this->assertCount(3, $content['licenses']);
|
||||
$this->assertGreaterThan(3, $content['total']);
|
||||
}
|
||||
|
||||
public function test_offset_skips_results_for_pagination()
|
||||
{
|
||||
License::factory()->count(10)->create();
|
||||
|
||||
$page1 = $this->handle(['limit' => 4, 'offset' => 0])->getStructuredContent();
|
||||
$page2 = $this->handle(['limit' => 4, 'offset' => 4])->getStructuredContent();
|
||||
|
||||
$ids1 = array_column($page1['licenses'], 'id');
|
||||
$ids2 = array_column($page2['licenses'], 'id');
|
||||
|
||||
$this->assertEmpty(array_intersect($ids1, $ids2));
|
||||
}
|
||||
|
||||
public function test_search_matches_on_name()
|
||||
{
|
||||
$target = License::factory()->create(['name' => 'UniqueLicenseXYZ']);
|
||||
License::factory()->create(['name' => 'Something Else']);
|
||||
|
||||
$content = $this->handle(['search' => 'UniqueLicenseXYZ'])->getStructuredContent();
|
||||
$ids = array_column($content['licenses'], 'id');
|
||||
|
||||
$this->assertContains($target->id, $ids);
|
||||
}
|
||||
|
||||
public function test_filters_by_category_id()
|
||||
{
|
||||
$category = Category::factory()->create(['category_type' => 'license']);
|
||||
$matching = License::factory()->create(['category_id' => $category->id]);
|
||||
$other = License::factory()->create();
|
||||
|
||||
$content = $this->handle(['category_id' => $category->id])->getStructuredContent();
|
||||
$ids = array_column($content['licenses'], 'id');
|
||||
|
||||
$this->assertContains($matching->id, $ids);
|
||||
$this->assertNotContains($other->id, $ids);
|
||||
}
|
||||
|
||||
public function test_filters_by_manufacturer_id()
|
||||
{
|
||||
$manufacturer = Manufacturer::factory()->create();
|
||||
$matching = License::factory()->create(['manufacturer_id' => $manufacturer->id]);
|
||||
$other = License::factory()->create(['manufacturer_id' => null]);
|
||||
|
||||
$content = $this->handle(['manufacturer_id' => $manufacturer->id])->getStructuredContent();
|
||||
$ids = array_column($content['licenses'], 'id');
|
||||
|
||||
$this->assertContains($matching->id, $ids);
|
||||
$this->assertNotContains($other->id, $ids);
|
||||
}
|
||||
|
||||
public function test_returns_error_when_user_lacks_permission()
|
||||
{
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$this->assertTrue($this->handle()->responses()->first()->isError());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Mcp;
|
||||
|
||||
use App\Mcp\Tools\ShowLicenseTool;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\User;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ShowLicenseToolTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->actingAs(User::factory()->viewLicenses()->create());
|
||||
}
|
||||
|
||||
private function handle(array $args): ResponseFactory
|
||||
{
|
||||
return (new ShowLicenseTool)->handle(new Request($args));
|
||||
}
|
||||
|
||||
public function test_returns_license_by_id()
|
||||
{
|
||||
$license = License::factory()->create(['name' => 'Test License', 'seats' => 5]);
|
||||
|
||||
$content = $this->handle(['id' => $license->id])->getStructuredContent();
|
||||
|
||||
$this->assertEquals($license->id, $content['id']);
|
||||
$this->assertEquals('Test License', $content['name']);
|
||||
$this->assertEquals(5, $content['seats']);
|
||||
}
|
||||
|
||||
public function test_returns_license_by_name()
|
||||
{
|
||||
$license = License::factory()->create(['name' => 'FindByName License']);
|
||||
|
||||
$content = $this->handle(['name' => 'FindByName License'])->getStructuredContent();
|
||||
|
||||
$this->assertEquals($license->id, $content['id']);
|
||||
$this->assertEquals('FindByName License', $content['name']);
|
||||
}
|
||||
|
||||
public function test_response_includes_seat_counts()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
|
||||
$user = User::factory()->create();
|
||||
|
||||
$seat = $license->freeSeat();
|
||||
$seat->assigned_to = $user->id;
|
||||
$seat->save();
|
||||
|
||||
$content = $this->handle(['id' => $license->id])->getStructuredContent();
|
||||
|
||||
$this->assertEquals(3, $content['seats']);
|
||||
$this->assertEquals(1, $content['assigned_seats']);
|
||||
$this->assertEquals(2, $content['free_seats']);
|
||||
}
|
||||
|
||||
public function test_response_includes_key_fields()
|
||||
{
|
||||
$license = License::factory()->create([
|
||||
'name' => 'Full Details License',
|
||||
'serial' => 'SN-12345',
|
||||
'license_name' => 'Acme Corp',
|
||||
'license_email' => 'legal@acme.com',
|
||||
]);
|
||||
|
||||
$content = $this->handle(['id' => $license->id])->getStructuredContent();
|
||||
|
||||
$this->assertArrayHasKey('serial', $content);
|
||||
$this->assertArrayHasKey('license_name', $content);
|
||||
$this->assertArrayHasKey('license_email', $content);
|
||||
$this->assertArrayHasKey('maintained', $content);
|
||||
$this->assertArrayHasKey('reassignable', $content);
|
||||
$this->assertArrayHasKey('category', $content);
|
||||
$this->assertArrayHasKey('category_id', $content);
|
||||
$this->assertEquals('SN-12345', $content['serial']);
|
||||
$this->assertEquals('Acme Corp', $content['license_name']);
|
||||
}
|
||||
|
||||
public function test_returns_error_when_no_identifier_provided()
|
||||
{
|
||||
$this->assertTrue($this->handle([])->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_returns_error_when_not_found()
|
||||
{
|
||||
$this->assertTrue($this->handle(['id' => 999999])->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_returns_error_when_user_lacks_permission()
|
||||
{
|
||||
$license = License::factory()->create();
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$this->assertTrue($this->handle(['id' => $license->id])->responses()->first()->isError());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Mcp;
|
||||
|
||||
use App\Mcp\Tools\UpdateLicenseTool;
|
||||
use App\Models\Category;
|
||||
use App\Models\License;
|
||||
use App\Models\User;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UpdateLicenseToolTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->actingAs(User::factory()->editLicenses()->create());
|
||||
}
|
||||
|
||||
private function handle(array $args): ResponseFactory
|
||||
{
|
||||
return (new UpdateLicenseTool)->handle(new Request($args));
|
||||
}
|
||||
|
||||
public function test_updates_license_by_id()
|
||||
{
|
||||
$license = License::factory()->create(['name' => 'Original Name']);
|
||||
|
||||
$content = $this->handle([
|
||||
'id' => $license->id,
|
||||
'new_name' => 'Updated Name',
|
||||
])->getStructuredContent();
|
||||
|
||||
$this->assertTrue($content['success']);
|
||||
$this->assertDatabaseHas('licenses', ['id' => $license->id, 'name' => 'Updated Name']);
|
||||
}
|
||||
|
||||
public function test_updates_license_by_name()
|
||||
{
|
||||
$license = License::factory()->create(['name' => 'Find By Name License']);
|
||||
|
||||
$content = $this->handle([
|
||||
'name' => 'Find By Name License',
|
||||
'new_name' => 'Renamed License',
|
||||
])->getStructuredContent();
|
||||
|
||||
$this->assertTrue($content['success']);
|
||||
$this->assertDatabaseHas('licenses', ['id' => $license->id, 'name' => 'Renamed License']);
|
||||
}
|
||||
|
||||
public function test_response_includes_id_name_and_seats()
|
||||
{
|
||||
$license = License::factory()->create(['seats' => 5]);
|
||||
|
||||
$content = $this->handle([
|
||||
'id' => $license->id,
|
||||
'new_name' => 'Response Check License',
|
||||
])->getStructuredContent();
|
||||
|
||||
$this->assertArrayHasKey('id', $content);
|
||||
$this->assertEquals('Response Check License', $content['name']);
|
||||
$this->assertEquals(5, $content['seats']);
|
||||
}
|
||||
|
||||
public function test_updates_multiple_fields()
|
||||
{
|
||||
$license = License::factory()->create();
|
||||
$category = Category::factory()->create(['category_type' => 'license']);
|
||||
|
||||
$this->handle([
|
||||
'id' => $license->id,
|
||||
'category_id' => $category->id,
|
||||
'serial' => 'SN-NEW-001',
|
||||
'purchase_cost' => 199.99,
|
||||
'expiration_date' => '2026-12-31',
|
||||
'maintained' => true,
|
||||
'reassignable' => true,
|
||||
'notes' => 'Updated notes',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('licenses', [
|
||||
'id' => $license->id,
|
||||
'category_id' => $category->id,
|
||||
'serial' => 'SN-NEW-001',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_returns_error_when_not_found()
|
||||
{
|
||||
$this->assertTrue($this->handle(['id' => 999999, 'new_name' => 'Ghost'])->responses()->first()->isError());
|
||||
}
|
||||
|
||||
public function test_returns_error_when_user_lacks_permission()
|
||||
{
|
||||
$license = License::factory()->create(['name' => 'Unchanged License']);
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$this->assertTrue($this->handle([
|
||||
'id' => $license->id,
|
||||
'new_name' => 'Should Not Change',
|
||||
])->responses()->first()->isError());
|
||||
|
||||
$this->assertDatabaseHas('licenses', ['id' => $license->id, 'name' => 'Unchanged License']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user