From 9c97a06c7e15af0fa6b17f5db71a7e07de31026b Mon Sep 17 00:00:00 2001 From: snipe Date: Fri, 8 May 2026 11:45:30 +0100 Subject: [PATCH] Additional tools --- app/Mcp/Servers/SnipeMCPServer.php | 159 +++++++++++++++++- app/Mcp/Tools/CheckoutConsumableTool.php | 113 +++++++++++++ app/Mcp/Tools/CreateAssetModelTool.php | 97 +++++++++++ app/Mcp/Tools/CreateAssetTool.php | 108 ++++++++++++ app/Mcp/Tools/CreateCategoryTool.php | 89 ++++++++++ app/Mcp/Tools/CreateCompanyTool.php | 90 ++++++++++ app/Mcp/Tools/CreateConsumableTool.php | 106 ++++++++++++ app/Mcp/Tools/CreateDepreciationTool.php | 74 ++++++++ app/Mcp/Tools/CreateGroupTool.php | 75 +++++++++ app/Mcp/Tools/CreateLocationTool.php | 97 +++++++++++ app/Mcp/Tools/CreateMaintenanceTool.php | 105 ++++++++++++ app/Mcp/Tools/CreateManufacturerTool.php | 83 +++++++++ app/Mcp/Tools/CreateStatusLabelTool.php | 95 +++++++++++ app/Mcp/Tools/CreateSupplierTool.php | 96 +++++++++++ app/Mcp/Tools/DeleteAssetModelTool.php | 83 +++++++++ app/Mcp/Tools/DeleteCategoryTool.php | 83 +++++++++ app/Mcp/Tools/DeleteCompanyTool.php | 79 +++++++++ app/Mcp/Tools/DeleteConsumableTool.php | 83 +++++++++ app/Mcp/Tools/DeleteDepreciationTool.php | 79 +++++++++ app/Mcp/Tools/DeleteGroupTool.php | 75 +++++++++ app/Mcp/Tools/DeleteLocationTool.php | 87 ++++++++++ app/Mcp/Tools/DeleteManufacturerTool.php | 79 +++++++++ app/Mcp/Tools/DeleteStatusLabelTool.php | 83 +++++++++ app/Mcp/Tools/DeleteSupplierTool.php | 79 +++++++++ app/Mcp/Tools/GetActivityLogTool.php | 102 +++++++++++ app/Mcp/Tools/GetCurrentUserTool.php | 72 ++++++++ app/Mcp/Tools/GetUserAssetsTool.php | 82 +++++++++ app/Mcp/Tools/ListAssetModelsTool.php | 101 +++++++++++ app/Mcp/Tools/ListCategoriesTool.php | 98 +++++++++++ app/Mcp/Tools/ListCompaniesTool.php | 88 ++++++++++ app/Mcp/Tools/ListConsumablesTool.php | 110 ++++++++++++ app/Mcp/Tools/ListDepreciationsTool.php | 82 +++++++++ app/Mcp/Tools/ListGroupsTool.php | 81 +++++++++ app/Mcp/Tools/ListLocationsTool.php | 101 +++++++++++ app/Mcp/Tools/ListMaintenancesTool.php | 89 ++++++++++ app/Mcp/Tools/ListManufacturersTool.php | 93 ++++++++++ app/Mcp/Tools/ListStatusLabelsTool.php | 99 +++++++++++ app/Mcp/Tools/ListSuppliersTool.php | 92 ++++++++++ app/Mcp/Tools/Reset2FATool.php | 68 ++++++++ app/Mcp/Tools/RestoreAssetTool.php | 69 ++++++++ app/Mcp/Tools/RestoreUserTool.php | 73 ++++++++ app/Mcp/Tools/ShowAssetModelTool.php | 105 ++++++++++++ app/Mcp/Tools/ShowCategoryTool.php | 95 +++++++++++ app/Mcp/Tools/ShowCompanyTool.php | 89 ++++++++++ app/Mcp/Tools/ShowConsumableTool.php | 107 ++++++++++++ app/Mcp/Tools/ShowDepreciationTool.php | 87 ++++++++++ app/Mcp/Tools/ShowGroupTool.php | 75 +++++++++ app/Mcp/Tools/ShowLocationTool.php | 109 ++++++++++++ app/Mcp/Tools/ShowManufacturerTool.php | 97 +++++++++++ app/Mcp/Tools/ShowStatusLabelTool.php | 101 +++++++++++ app/Mcp/Tools/ShowSupplierTool.php | 111 ++++++++++++ app/Mcp/Tools/UpdateAssetModelTool.php | 111 ++++++++++++ app/Mcp/Tools/UpdateCategoryTool.php | 105 ++++++++++++ app/Mcp/Tools/UpdateCompanyTool.php | 101 +++++++++++ app/Mcp/Tools/UpdateConsumableTool.php | 120 +++++++++++++ app/Mcp/Tools/UpdateDepreciationTool.php | 95 +++++++++++ app/Mcp/Tools/UpdateGroupTool.php | 92 ++++++++++ app/Mcp/Tools/UpdateLocationTool.php | 113 +++++++++++++ app/Mcp/Tools/UpdateManufacturerTool.php | 107 ++++++++++++ app/Mcp/Tools/UpdateStatusLabelTool.php | 122 ++++++++++++++ app/Mcp/Tools/UpdateSupplierTool.php | 119 +++++++++++++ database/factories/UserFactory.php | 110 +++++++++++- .../Feature/Mcp/CreateAssetModelToolTest.php | 1 - tests/Feature/Mcp/CreateAssetToolTest.php | 1 - .../Mcp/CreateDepreciationToolTest.php | 1 - tests/Feature/Mcp/CreateGroupToolTest.php | 6 +- tests/Feature/Mcp/UpdateGroupToolTest.php | 4 +- 67 files changed, 5866 insertions(+), 15 deletions(-) create mode 100644 app/Mcp/Tools/CheckoutConsumableTool.php create mode 100644 app/Mcp/Tools/CreateAssetModelTool.php create mode 100644 app/Mcp/Tools/CreateAssetTool.php create mode 100644 app/Mcp/Tools/CreateCategoryTool.php create mode 100644 app/Mcp/Tools/CreateCompanyTool.php create mode 100644 app/Mcp/Tools/CreateConsumableTool.php create mode 100644 app/Mcp/Tools/CreateDepreciationTool.php create mode 100644 app/Mcp/Tools/CreateGroupTool.php create mode 100644 app/Mcp/Tools/CreateLocationTool.php create mode 100644 app/Mcp/Tools/CreateMaintenanceTool.php create mode 100644 app/Mcp/Tools/CreateManufacturerTool.php create mode 100644 app/Mcp/Tools/CreateStatusLabelTool.php create mode 100644 app/Mcp/Tools/CreateSupplierTool.php create mode 100644 app/Mcp/Tools/DeleteAssetModelTool.php create mode 100644 app/Mcp/Tools/DeleteCategoryTool.php create mode 100644 app/Mcp/Tools/DeleteCompanyTool.php create mode 100644 app/Mcp/Tools/DeleteConsumableTool.php create mode 100644 app/Mcp/Tools/DeleteDepreciationTool.php create mode 100644 app/Mcp/Tools/DeleteGroupTool.php create mode 100644 app/Mcp/Tools/DeleteLocationTool.php create mode 100644 app/Mcp/Tools/DeleteManufacturerTool.php create mode 100644 app/Mcp/Tools/DeleteStatusLabelTool.php create mode 100644 app/Mcp/Tools/DeleteSupplierTool.php create mode 100644 app/Mcp/Tools/GetActivityLogTool.php create mode 100644 app/Mcp/Tools/GetCurrentUserTool.php create mode 100644 app/Mcp/Tools/GetUserAssetsTool.php create mode 100644 app/Mcp/Tools/ListAssetModelsTool.php create mode 100644 app/Mcp/Tools/ListCategoriesTool.php create mode 100644 app/Mcp/Tools/ListCompaniesTool.php create mode 100644 app/Mcp/Tools/ListConsumablesTool.php create mode 100644 app/Mcp/Tools/ListDepreciationsTool.php create mode 100644 app/Mcp/Tools/ListGroupsTool.php create mode 100644 app/Mcp/Tools/ListLocationsTool.php create mode 100644 app/Mcp/Tools/ListMaintenancesTool.php create mode 100644 app/Mcp/Tools/ListManufacturersTool.php create mode 100644 app/Mcp/Tools/ListStatusLabelsTool.php create mode 100644 app/Mcp/Tools/ListSuppliersTool.php create mode 100644 app/Mcp/Tools/Reset2FATool.php create mode 100644 app/Mcp/Tools/RestoreAssetTool.php create mode 100644 app/Mcp/Tools/RestoreUserTool.php create mode 100644 app/Mcp/Tools/ShowAssetModelTool.php create mode 100644 app/Mcp/Tools/ShowCategoryTool.php create mode 100644 app/Mcp/Tools/ShowCompanyTool.php create mode 100644 app/Mcp/Tools/ShowConsumableTool.php create mode 100644 app/Mcp/Tools/ShowDepreciationTool.php create mode 100644 app/Mcp/Tools/ShowGroupTool.php create mode 100644 app/Mcp/Tools/ShowLocationTool.php create mode 100644 app/Mcp/Tools/ShowManufacturerTool.php create mode 100644 app/Mcp/Tools/ShowStatusLabelTool.php create mode 100644 app/Mcp/Tools/ShowSupplierTool.php create mode 100644 app/Mcp/Tools/UpdateAssetModelTool.php create mode 100644 app/Mcp/Tools/UpdateCategoryTool.php create mode 100644 app/Mcp/Tools/UpdateCompanyTool.php create mode 100644 app/Mcp/Tools/UpdateConsumableTool.php create mode 100644 app/Mcp/Tools/UpdateDepreciationTool.php create mode 100644 app/Mcp/Tools/UpdateGroupTool.php create mode 100644 app/Mcp/Tools/UpdateLocationTool.php create mode 100644 app/Mcp/Tools/UpdateManufacturerTool.php create mode 100644 app/Mcp/Tools/UpdateStatusLabelTool.php create mode 100644 app/Mcp/Tools/UpdateSupplierTool.php diff --git a/app/Mcp/Servers/SnipeMCPServer.php b/app/Mcp/Servers/SnipeMCPServer.php index c9345a2082..9716baef30 100644 --- a/app/Mcp/Servers/SnipeMCPServer.php +++ b/app/Mcp/Servers/SnipeMCPServer.php @@ -10,29 +10,89 @@ use App\Mcp\Tools\CheckinLicenseTool; use App\Mcp\Tools\CheckoutAccessoryTool; use App\Mcp\Tools\CheckoutAssetTool; use App\Mcp\Tools\CheckoutComponentTool; +use App\Mcp\Tools\CheckoutConsumableTool; use App\Mcp\Tools\CheckoutLicenseTool; use App\Mcp\Tools\CreateAccessoryTool; +use App\Mcp\Tools\CreateAssetModelTool; +use App\Mcp\Tools\CreateAssetTool; +use App\Mcp\Tools\CreateCategoryTool; +use App\Mcp\Tools\CreateCompanyTool; use App\Mcp\Tools\CreateComponentTool; +use App\Mcp\Tools\CreateConsumableTool; use App\Mcp\Tools\CreateDepartmentTool; +use App\Mcp\Tools\CreateDepreciationTool; +use App\Mcp\Tools\CreateGroupTool; use App\Mcp\Tools\CreateLicenseTool; +use App\Mcp\Tools\CreateLocationTool; +use App\Mcp\Tools\CreateMaintenanceTool; +use App\Mcp\Tools\CreateManufacturerTool; +use App\Mcp\Tools\CreateStatusLabelTool; +use App\Mcp\Tools\CreateSupplierTool; use App\Mcp\Tools\CreateUserTool; use App\Mcp\Tools\DeleteAccessoryTool; +use App\Mcp\Tools\DeleteAssetModelTool; use App\Mcp\Tools\DeleteAssetTool; +use App\Mcp\Tools\DeleteCategoryTool; +use App\Mcp\Tools\DeleteCompanyTool; use App\Mcp\Tools\DeleteComponentTool; +use App\Mcp\Tools\DeleteConsumableTool; use App\Mcp\Tools\DeleteDepartmentTool; +use App\Mcp\Tools\DeleteDepreciationTool; +use App\Mcp\Tools\DeleteGroupTool; use App\Mcp\Tools\DeleteLicenseTool; +use App\Mcp\Tools\DeleteLocationTool; +use App\Mcp\Tools\DeleteManufacturerTool; +use App\Mcp\Tools\DeleteStatusLabelTool; +use App\Mcp\Tools\DeleteSupplierTool; use App\Mcp\Tools\DeleteUserTool; +use App\Mcp\Tools\GetActivityLogTool; +use App\Mcp\Tools\GetCurrentUserTool; +use App\Mcp\Tools\GetUserAssetsTool; +use App\Mcp\Tools\ListAssetModelsTool; use App\Mcp\Tools\ListAssetsTool; +use App\Mcp\Tools\ListCategoriesTool; +use App\Mcp\Tools\ListCompaniesTool; +use App\Mcp\Tools\ListConsumablesTool; +use App\Mcp\Tools\ListDepreciationsTool; +use App\Mcp\Tools\ListGroupsTool; use App\Mcp\Tools\ListLicensesTool; +use App\Mcp\Tools\ListLocationsTool; +use App\Mcp\Tools\ListMaintenancesTool; +use App\Mcp\Tools\ListManufacturersTool; +use App\Mcp\Tools\ListStatusLabelsTool; +use App\Mcp\Tools\ListSuppliersTool; use App\Mcp\Tools\ListUsersTool; +use App\Mcp\Tools\Reset2FATool; +use App\Mcp\Tools\RestoreAssetTool; +use App\Mcp\Tools\RestoreUserTool; +use App\Mcp\Tools\ShowAssetModelTool; use App\Mcp\Tools\ShowAssetTool; +use App\Mcp\Tools\ShowCategoryTool; +use App\Mcp\Tools\ShowCompanyTool; +use App\Mcp\Tools\ShowConsumableTool; +use App\Mcp\Tools\ShowDepreciationTool; +use App\Mcp\Tools\ShowGroupTool; use App\Mcp\Tools\ShowLicenseTool; +use App\Mcp\Tools\ShowLocationTool; +use App\Mcp\Tools\ShowManufacturerTool; +use App\Mcp\Tools\ShowStatusLabelTool; +use App\Mcp\Tools\ShowSupplierTool; use App\Mcp\Tools\ShowUserTool; use App\Mcp\Tools\UpdateAccessoryTool; +use App\Mcp\Tools\UpdateAssetModelTool; use App\Mcp\Tools\UpdateAssetTool; +use App\Mcp\Tools\UpdateCategoryTool; +use App\Mcp\Tools\UpdateCompanyTool; use App\Mcp\Tools\UpdateComponentTool; +use App\Mcp\Tools\UpdateConsumableTool; use App\Mcp\Tools\UpdateDepartmentTool; +use App\Mcp\Tools\UpdateDepreciationTool; +use App\Mcp\Tools\UpdateGroupTool; use App\Mcp\Tools\UpdateLicenseTool; +use App\Mcp\Tools\UpdateLocationTool; +use App\Mcp\Tools\UpdateManufacturerTool; +use App\Mcp\Tools\UpdateStatusLabelTool; +use App\Mcp\Tools\UpdateSupplierTool; use App\Mcp\Tools\UpdateUserTool; use Laravel\Mcp\Server; use Laravel\Mcp\Server\Attributes\Instructions; @@ -45,28 +105,51 @@ use Laravel\Mcp\Server\Attributes\Version; class SnipeMCPServer extends Server { protected array $tools = [ + // Assets ShowAssetTool::class, ListAssetsTool::class, - CheckoutAssetTool::class, - CheckinAssetTool::class, + CreateAssetTool::class, UpdateAssetTool::class, DeleteAssetTool::class, + RestoreAssetTool::class, + CheckoutAssetTool::class, + CheckinAssetTool::class, AuditAssetTool::class, + + // Users ListUsersTool::class, ShowUserTool::class, CreateUserTool::class, UpdateUserTool::class, DeleteUserTool::class, + RestoreUserTool::class, + GetCurrentUserTool::class, + GetUserAssetsTool::class, + Reset2FATool::class, + + // Accessories CreateAccessoryTool::class, UpdateAccessoryTool::class, DeleteAccessoryTool::class, CheckoutAccessoryTool::class, CheckinAccessoryTool::class, + + // Components CreateComponentTool::class, UpdateComponentTool::class, DeleteComponentTool::class, CheckoutComponentTool::class, CheckinComponentTool::class, + + // Consumables + ListConsumablesTool::class, + ShowConsumableTool::class, + CreateConsumableTool::class, + UpdateConsumableTool::class, + DeleteConsumableTool::class, + CheckoutConsumableTool::class, + + // Licenses ListLicensesTool::class, ShowLicenseTool::class, CreateLicenseTool::class, @@ -74,9 +157,81 @@ class SnipeMCPServer extends Server DeleteLicenseTool::class, CheckoutLicenseTool::class, CheckinLicenseTool::class, + + // Departments CreateDepartmentTool::class, UpdateDepartmentTool::class, DeleteDepartmentTool::class, + + // Companies + ListCompaniesTool::class, + ShowCompanyTool::class, + CreateCompanyTool::class, + UpdateCompanyTool::class, + DeleteCompanyTool::class, + + // Categories + ListCategoriesTool::class, + ShowCategoryTool::class, + CreateCategoryTool::class, + UpdateCategoryTool::class, + DeleteCategoryTool::class, + + // Manufacturers + ListManufacturersTool::class, + ShowManufacturerTool::class, + CreateManufacturerTool::class, + UpdateManufacturerTool::class, + DeleteManufacturerTool::class, + + // Suppliers + ListSuppliersTool::class, + ShowSupplierTool::class, + CreateSupplierTool::class, + UpdateSupplierTool::class, + DeleteSupplierTool::class, + + // Status Labels + ListStatusLabelsTool::class, + ShowStatusLabelTool::class, + CreateStatusLabelTool::class, + UpdateStatusLabelTool::class, + DeleteStatusLabelTool::class, + + // Locations + ListLocationsTool::class, + ShowLocationTool::class, + CreateLocationTool::class, + UpdateLocationTool::class, + DeleteLocationTool::class, + + // Asset Models + ListAssetModelsTool::class, + ShowAssetModelTool::class, + CreateAssetModelTool::class, + UpdateAssetModelTool::class, + DeleteAssetModelTool::class, + + // Depreciations + ListDepreciationsTool::class, + ShowDepreciationTool::class, + CreateDepreciationTool::class, + UpdateDepreciationTool::class, + DeleteDepreciationTool::class, + + // Groups + ListGroupsTool::class, + ShowGroupTool::class, + CreateGroupTool::class, + UpdateGroupTool::class, + DeleteGroupTool::class, + + // Maintenance + ListMaintenancesTool::class, + CreateMaintenanceTool::class, + + // Activity Log + GetActivityLogTool::class, ]; protected array $resources = [ diff --git a/app/Mcp/Tools/CheckoutConsumableTool.php b/app/Mcp/Tools/CheckoutConsumableTool.php new file mode 100644 index 0000000000..6ac8dfdb56 --- /dev/null +++ b/app/Mcp/Tools/CheckoutConsumableTool.php @@ -0,0 +1,113 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + 'assigned_to' => 'required|integer', + 'note' => 'nullable|string|max:65535', + ]); + + $consumable = $this->resolveConsumable($request); + + if (! $consumable) { + return Response::make(Response::error('Consumable not found')); + } + + if (! Gate::allows('checkout', $consumable)) { + return Response::make(Response::error('Unauthorized')); + } + + if ($consumable->numRemaining() <= 0) { + return Response::make(Response::error('No units remaining')); + } + + $user = User::find($request->get('assigned_to')); + + if (! $user) { + return Response::make(Response::error('User not found')); + } + + $consumable->users()->attach($consumable->id, [ + 'consumable_id' => $consumable->id, + 'created_by' => auth()->id(), + 'assigned_to' => $user->id, + 'note' => $request->get('note'), + ]); + + event(new CheckoutableCheckedOut( + $consumable, + $user, + auth()->user(), + $request->get('note'), + [], + 1, + )); + + return Response::make( + Response::text('Consumable '.$consumable->name.' checked out to '.$user->username) + )->withStructuredContent([ + 'success' => true, + 'message' => 'Consumable checked out successfully', + 'consumable_id' => $consumable->id, + 'consumable_name' => $consumable->name, + 'assigned_to_id' => $user->id, + 'assigned_to_username' => $user->username, + ]); + } + + private function resolveConsumable(Request $request): ?Consumable + { + if ($request->filled('id')) { + return Consumable::find($request->get('id')); + } + if ($request->filled('name')) { + return Consumable::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the consumable to check out'), + 'name' => $schema->string()->description('Name of the consumable to check out'), + 'assigned_to' => $schema->number()->description('User ID to check out to (required)'), + '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(), + 'consumable_id' => $schema->number()->description('Numeric ID of the consumable'), + 'consumable_name' => $schema->string()->description('Name of the consumable'), + 'assigned_to_id' => $schema->number()->description('ID of the user the consumable was checked out to'), + 'assigned_to_username' => $schema->string()->description('Username of the user the consumable was checked out to'), + ]; + } +} diff --git a/app/Mcp/Tools/CreateAssetModelTool.php b/app/Mcp/Tools/CreateAssetModelTool.php new file mode 100644 index 0000000000..f9352195e2 --- /dev/null +++ b/app/Mcp/Tools/CreateAssetModelTool.php @@ -0,0 +1,97 @@ +validate([ + 'name' => 'required|string|max:255', + 'category_id' => 'required|integer|exists:categories,id', + 'model_number' => 'nullable|string|max:255', + 'manufacturer_id' => 'nullable|integer|exists:manufacturers,id', + 'depreciation_id' => 'nullable|integer|exists:depreciations,id', + 'eol' => 'nullable|integer|min:0|max:240', + 'min_amt' => 'nullable|integer|min:0', + 'notes' => 'nullable|string', + 'requestable' => 'nullable|boolean', + 'require_serial' => 'nullable|boolean', + ]); + } catch (ValidationException $e) { + return Response::make(Response::error($e->validator->errors()->first())); + } + + $assetModel = new AssetModel; + $assetModel->name = $request->get('name'); + $assetModel->category_id = $request->get('category_id'); + $assetModel->created_by = auth()->id(); + + foreach (['model_number', 'manufacturer_id', 'depreciation_id', 'eol', 'min_amt', 'notes', 'requestable', 'require_serial'] as $f) { + if ($request->filled($f)) { + $assetModel->{$f} = $request->get($f); + } + } + + if ($assetModel->save()) { + return Response::make( + Response::text('Asset model '.$assetModel->name.' created successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Asset model created successfully', + 'id' => $assetModel->id, + 'name' => $assetModel->name, + 'category_id' => $assetModel->category_id, + ]); + } + + return Response::make(Response::error('Create failed: '.$assetModel->getErrors()->first())); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'name' => $schema->string()->description('Asset model name (required)'), + 'category_id' => $schema->number()->description('Category ID (required)'), + 'model_number' => $schema->string()->description('Model number'), + 'manufacturer_id' => $schema->number()->description('Manufacturer ID'), + 'depreciation_id' => $schema->number()->description('Depreciation schedule ID'), + 'eol' => $schema->number()->description('End of life in months (0-240)'), + 'min_amt' => $schema->number()->description('Minimum quantity alert threshold'), + 'notes' => $schema->string()->description('Notes'), + 'requestable' => $schema->boolean()->description('Whether the model can be requested'), + 'require_serial' => $schema->boolean()->description('Whether serial numbers are required'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the asset model was created'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the new asset model'), + 'name' => $schema->string()->description('Name of the new asset model'), + 'category_id' => $schema->number()->description('Category ID of the new asset model'), + ]; + } +} diff --git a/app/Mcp/Tools/CreateAssetTool.php b/app/Mcp/Tools/CreateAssetTool.php new file mode 100644 index 0000000000..b837dee517 --- /dev/null +++ b/app/Mcp/Tools/CreateAssetTool.php @@ -0,0 +1,108 @@ +validate([ + 'model_id' => 'required|integer|exists:models,id', + 'status_id' => 'required|integer|exists:status_labels,id', + 'asset_tag' => 'required|string|max:255', + 'name' => 'nullable|string|max:255', + 'serial' => 'nullable|string', + 'company_id' => 'nullable|integer', + 'location_id' => 'nullable|integer|exists:locations,id', + 'rtd_location_id' => 'nullable|integer|exists:locations,id', + 'supplier_id' => 'nullable|integer|exists:suppliers,id', + 'purchase_date' => 'nullable|date_format:Y-m-d', + 'purchase_cost' => 'nullable|numeric', + 'order_number' => 'nullable|string|max:191', + 'warranty_months' => 'nullable|integer|min:0|max:240', + 'requestable' => 'nullable|boolean', + 'notes' => 'nullable|string|max:65535', + ]); + } catch (ValidationException $e) { + return Response::make(Response::error($e->validator->errors()->first())); + } + + $asset = new Asset; + $asset->model_id = $request->get('model_id'); + $asset->status_id = $request->get('status_id'); + $asset->asset_tag = $request->get('asset_tag'); + $asset->created_by = auth()->id(); + + foreach (['name', 'serial', 'company_id', 'location_id', 'rtd_location_id', 'supplier_id', 'purchase_date', 'purchase_cost', 'order_number', 'warranty_months', 'requestable', 'notes'] as $field) { + if ($request->filled($field)) { + $asset->{$field} = $request->get($field); + } + } + + if ($asset->save()) { + return Response::make( + Response::text('Asset '.$asset->asset_tag.' created successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Asset created successfully', + 'id' => $asset->id, + 'asset_tag' => $asset->asset_tag, + 'name' => $asset->name, + ]); + } + + return Response::make(Response::error('Create failed: '.$asset->getErrors()->first())); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'model_id' => $schema->number()->description('Asset model ID (required)'), + 'status_id' => $schema->number()->description('Status label ID (required)'), + 'asset_tag' => $schema->string()->description('Asset tag (required)'), + 'name' => $schema->string()->description('Display name for the asset'), + 'serial' => $schema->string()->description('Serial number'), + 'company_id' => $schema->number()->description('Company ID'), + 'location_id' => $schema->number()->description('Current location ID'), + 'rtd_location_id' => $schema->number()->description('Default RTD location ID'), + 'supplier_id' => $schema->number()->description('Supplier ID'), + 'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'), + 'purchase_cost' => $schema->number()->description('Purchase cost'), + 'order_number' => $schema->string()->description('Order number'), + 'warranty_months' => $schema->number()->description('Warranty length in months (0-240)'), + 'requestable' => $schema->boolean()->description('Whether the asset is user-requestable'), + 'notes' => $schema->string()->description('Notes'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the asset was created'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the new asset'), + 'asset_tag' => $schema->string()->description('Asset tag of the new asset'), + 'name' => $schema->string()->description('Display name of the new asset'), + ]; + } +} diff --git a/app/Mcp/Tools/CreateCategoryTool.php b/app/Mcp/Tools/CreateCategoryTool.php new file mode 100644 index 0000000000..c50bb14ed3 --- /dev/null +++ b/app/Mcp/Tools/CreateCategoryTool.php @@ -0,0 +1,89 @@ +validate([ + 'name' => 'required|string|max:255', + 'category_type' => 'required|string|in:asset,accessory,consumable,component,license', + 'checkin_email' => 'nullable|boolean', + 'require_acceptance' => 'nullable|boolean', + 'use_default_eula' => 'nullable|boolean', + 'notes' => 'nullable|string', + ]); + } catch (ValidationException $e) { + return Response::make(Response::error($e->validator->errors()->first())); + } + + $category = new Category; + $category->name = $request->get('name'); + $category->category_type = $request->get('category_type'); + $category->created_by = auth()->id(); + + foreach (['checkin_email', 'require_acceptance', 'use_default_eula', 'notes'] as $field) { + if ($request->filled($field)) { + $category->{$field} = $request->get($field); + } + } + + if ($category->save()) { + return Response::make( + Response::text('Category '.$category->name.' created successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Category created successfully', + 'id' => $category->id, + 'name' => $category->name, + 'category_type' => $category->category_type, + ]); + } + + return Response::make(Response::error('Create failed: '.$category->getErrors()->first())); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'name' => $schema->string()->description('Category name (required)'), + 'category_type' => $schema->string()->description('Category type (required): asset, accessory, consumable, component, or license'), + 'checkin_email' => $schema->boolean()->description('Send checkin email when items are checked in'), + 'require_acceptance' => $schema->boolean()->description('Require user acceptance when checking out'), + 'use_default_eula' => $schema->boolean()->description('Use the default EULA'), + 'notes' => $schema->string()->description('Notes'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the category was created'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the new category'), + 'name' => $schema->string()->description('Name of the new category'), + 'category_type' => $schema->string()->description('Type of the new category'), + ]; + } +} diff --git a/app/Mcp/Tools/CreateCompanyTool.php b/app/Mcp/Tools/CreateCompanyTool.php new file mode 100644 index 0000000000..b9ab3fd777 --- /dev/null +++ b/app/Mcp/Tools/CreateCompanyTool.php @@ -0,0 +1,90 @@ +validate([ + 'name' => 'required|string|max:255', + 'phone' => 'nullable|string', + 'fax' => 'nullable|string', + 'email' => 'nullable|string', + 'notes' => 'nullable|string', + ]); + } catch (ValidationException $e) { + return Response::make(Response::error($e->validator->errors()->first())); + } + + $company = new Company; + $company->name = $request->get('name'); + if ($request->filled('phone')) { + $company->phone = $request->get('phone'); + } + if ($request->filled('fax')) { + $company->fax = $request->get('fax'); + } + if ($request->filled('email')) { + $company->email = $request->get('email'); + } + if ($request->filled('notes')) { + $company->notes = $request->get('notes'); + } + $company->created_by = auth()->id(); + + if ($company->save()) { + return Response::make( + Response::text('Company '.$company->name.' created successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Company created successfully', + 'id' => $company->id, + 'name' => $company->name, + ]); + } + + return Response::make(Response::error('Create failed: '.$company->getErrors()->first())); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'name' => $schema->string()->description('Company name (required)'), + 'phone' => $schema->string()->description('Company phone number'), + 'fax' => $schema->string()->description('Company fax number'), + 'email' => $schema->string()->description('Company email address'), + 'notes' => $schema->string()->description('Notes'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the company was created'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the new company'), + 'name' => $schema->string()->description('Name of the new company'), + ]; + } +} diff --git a/app/Mcp/Tools/CreateConsumableTool.php b/app/Mcp/Tools/CreateConsumableTool.php new file mode 100644 index 0000000000..48cc2a51ae --- /dev/null +++ b/app/Mcp/Tools/CreateConsumableTool.php @@ -0,0 +1,106 @@ +validate([ + 'name' => 'required|string|max:255', + 'qty' => 'required|integer|min:0', + 'category_id' => 'required|integer|exists:categories,id', + 'company_id' => 'nullable|integer', + 'location_id' => 'nullable|integer|exists:locations,id', + 'manufacturer_id' => 'nullable|integer|exists:manufacturers,id', + 'supplier_id' => 'nullable|integer|exists:suppliers,id', + 'item_no' => 'nullable|string|max:255', + 'order_number' => 'nullable|string|max:255', + 'model_number' => 'nullable|string|max:255', + 'purchase_cost' => 'nullable|numeric|min:0', + 'purchase_date' => 'nullable|date_format:Y-m-d', + 'min_amt' => 'nullable|integer|min:0', + 'requestable' => 'nullable|boolean', + 'notes' => 'nullable|string', + ]); + } catch (ValidationException $e) { + return Response::make(Response::error($e->validator->errors()->first())); + } + + $consumable = new Consumable; + $consumable->fill($request->only([ + 'name', 'qty', 'category_id', 'company_id', 'location_id', 'manufacturer_id', + 'supplier_id', 'item_no', 'order_number', 'model_number', 'purchase_cost', + 'purchase_date', 'min_amt', 'requestable', 'notes', + ])); + $consumable->created_by = auth()->id(); + + if ($consumable->save()) { + return Response::make( + Response::text('Consumable '.$consumable->name.' created successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Consumable created successfully', + 'id' => $consumable->id, + 'name' => $consumable->name, + 'qty' => $consumable->qty, + 'category_id' => $consumable->category_id, + ]); + } + + return Response::make(Response::error('Create failed: '.$consumable->getErrors()->first())); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'name' => $schema->string()->description('Consumable name (required)'), + 'qty' => $schema->number()->description('Total quantity in stock (required)'), + 'category_id' => $schema->number()->description('Category ID — must be a consumable category (required)'), + 'company_id' => $schema->number()->description('Company ID'), + 'location_id' => $schema->number()->description('Location ID'), + 'manufacturer_id' => $schema->number()->description('Manufacturer ID'), + 'supplier_id' => $schema->number()->description('Supplier ID'), + 'item_no' => $schema->string()->description('Item number'), + 'order_number' => $schema->string()->description('Order number'), + 'model_number' => $schema->string()->description('Model number'), + 'purchase_cost' => $schema->number()->description('Purchase cost per unit'), + 'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'), + 'min_amt' => $schema->number()->description('Minimum quantity threshold for alerts'), + 'requestable' => $schema->boolean()->description('Whether users can request this consumable'), + 'notes' => $schema->string()->description('Notes'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the consumable was created'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the new consumable'), + 'name' => $schema->string()->description('Name of the new consumable'), + 'qty' => $schema->number()->description('Total quantity'), + 'category_id' => $schema->number()->description('Category ID'), + ]; + } +} diff --git a/app/Mcp/Tools/CreateDepreciationTool.php b/app/Mcp/Tools/CreateDepreciationTool.php new file mode 100644 index 0000000000..9894f59dc8 --- /dev/null +++ b/app/Mcp/Tools/CreateDepreciationTool.php @@ -0,0 +1,74 @@ +validate([ + 'name' => 'required|string|max:255', + 'months' => 'required|integer|min:1|max:3600', + ]); + } catch (ValidationException $e) { + return Response::make(Response::error($e->validator->errors()->first())); + } + + $depreciation = new Depreciation; + $depreciation->name = $request->get('name'); + $depreciation->months = $request->get('months'); + + if ($depreciation->save()) { + return Response::make( + Response::text('Depreciation '.$depreciation->name.' created successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Depreciation created successfully', + 'id' => $depreciation->id, + 'name' => $depreciation->name, + 'months' => $depreciation->months, + ]); + } + + return Response::make(Response::error('Create failed: '.$depreciation->getErrors()->first())); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'name' => $schema->string()->description('Depreciation name (required)'), + 'months' => $schema->number()->description('Depreciation period in months (required, 1-3600)'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the depreciation was created'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the new depreciation'), + 'name' => $schema->string()->description('Name of the new depreciation'), + 'months' => $schema->number()->description('Depreciation period in months'), + ]; + } +} diff --git a/app/Mcp/Tools/CreateGroupTool.php b/app/Mcp/Tools/CreateGroupTool.php new file mode 100644 index 0000000000..2819163907 --- /dev/null +++ b/app/Mcp/Tools/CreateGroupTool.php @@ -0,0 +1,75 @@ +validate([ + 'name' => 'required|string|max:255', + 'notes' => 'nullable|string', + ]); + } catch (ValidationException $e) { + return Response::make(Response::error($e->validator->errors()->first())); + } + + $group = new Group; + $group->name = $request->get('name'); + if ($request->filled('notes')) { + $group->notes = $request->get('notes'); + } + $group->created_by = auth()->id(); + + if ($group->save()) { + return Response::make( + Response::text('Group '.$group->name.' created successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Group created successfully', + 'id' => $group->id, + 'name' => $group->name, + ]); + } + + return Response::make(Response::error('Create failed: '.$group->getErrors()->first())); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'name' => $schema->string()->description('Group name (required, must be unique)'), + 'notes' => $schema->string()->description('Notes about the group'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the group was created'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the new group'), + 'name' => $schema->string()->description('Name of the new group'), + ]; + } +} diff --git a/app/Mcp/Tools/CreateLocationTool.php b/app/Mcp/Tools/CreateLocationTool.php new file mode 100644 index 0000000000..fabc7fbf62 --- /dev/null +++ b/app/Mcp/Tools/CreateLocationTool.php @@ -0,0 +1,97 @@ +validate([ + 'name' => 'required|string|max:255', + 'address' => 'nullable|string', + 'address2' => 'nullable|string', + 'city' => 'nullable|string', + 'state' => 'nullable|string', + 'country' => 'nullable|string', + 'zip' => 'nullable|string', + 'phone' => 'nullable|string|max:255', + 'fax' => 'nullable|string|max:255', + 'currency' => 'nullable|string', + 'parent_id' => 'nullable|integer|exists:locations,id', + 'manager_id' => 'nullable|integer|exists:users,id', + ]); + } catch (ValidationException $e) { + return Response::make(Response::error($e->validator->errors()->first())); + } + + $location = new Location; + $location->name = $request->get('name'); + + foreach (['address', 'address2', 'city', 'state', 'country', 'zip', 'phone', 'fax', 'currency', 'parent_id', 'manager_id'] as $field) { + if ($request->filled($field)) { + $location->{$field} = $request->get($field); + } + } + + if ($location->save()) { + return Response::make( + Response::text('Location '.$location->name.' created successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Location created successfully', + 'id' => $location->id, + 'name' => $location->name, + ]); + } + + return Response::make(Response::error('Create failed: '.$location->getErrors()->first())); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'name' => $schema->string()->description('Location name (required)'), + 'address' => $schema->string()->description('Street address'), + 'address2' => $schema->string()->description('Address line 2'), + 'city' => $schema->string()->description('City'), + 'state' => $schema->string()->description('State'), + 'country' => $schema->string()->description('Country'), + 'zip' => $schema->string()->description('Zip code'), + 'phone' => $schema->string()->description('Phone number'), + 'fax' => $schema->string()->description('Fax number'), + 'currency' => $schema->string()->description('Currency code'), + 'parent_id' => $schema->number()->description('Parent location ID'), + 'manager_id' => $schema->number()->description('Manager user ID'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the location was created'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the new location'), + 'name' => $schema->string()->description('Name of the new location'), + ]; + } +} diff --git a/app/Mcp/Tools/CreateMaintenanceTool.php b/app/Mcp/Tools/CreateMaintenanceTool.php new file mode 100644 index 0000000000..cf89e3f8cf --- /dev/null +++ b/app/Mcp/Tools/CreateMaintenanceTool.php @@ -0,0 +1,105 @@ +validate([ + 'asset_id' => 'required|integer|exists:assets,id', + 'title' => 'required|string|max:255', + 'asset_maintenance_type' => 'nullable|string|max:255', + 'supplier_id' => 'nullable|integer|exists:suppliers,id', + 'is_warranty' => 'nullable|boolean', + 'cost' => 'nullable|numeric|min:0', + 'start_date' => 'nullable|date_format:Y-m-d', + 'completion_date' => 'nullable|date_format:Y-m-d', + 'notes' => 'nullable|string', + 'user_id' => 'nullable|integer|exists:users,id', + ]); + } catch (ValidationException $e) { + return Response::make(Response::error($e->validator->errors()->first())); + } + + $maintenance = new Maintenance; + $maintenance->asset_id = $request->get('asset_id'); + $maintenance->name = $request->get('title'); + $maintenance->asset_maintenance_type = $request->get('asset_maintenance_type', 'Maintenance'); + $maintenance->start_date = $request->filled('start_date') ? $request->get('start_date') : now()->format('Y-m-d'); + $maintenance->created_by = auth()->id(); + $maintenance->is_warranty = 0; + + foreach (['supplier_id', 'is_warranty', 'cost', 'completion_date', 'notes', 'user_id'] as $field) { + if ($request->filled($field)) { + $maintenance->{$field} = $request->get($field); + } + } + + if ($maintenance->save()) { + $maintenance->load('asset'); + + return Response::make( + Response::text('Maintenance '.$maintenance->name.' created successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Maintenance created successfully', + 'id' => $maintenance->id, + 'title' => $maintenance->name, + 'asset_id' => $maintenance->asset_id, + 'asset_tag' => $maintenance->asset?->asset_tag, + ]); + } + + return Response::make(Response::error('Create failed: '.$maintenance->getErrors()->first())); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'asset_id' => $schema->number()->description('Asset ID the maintenance is for (required)'), + 'title' => $schema->string()->description('Maintenance title/name (required)'), + 'asset_maintenance_type' => $schema->string()->description('Type of maintenance (e.g. maintenance, repair, upgrade)'), + 'supplier_id' => $schema->number()->description('Supplier ID'), + 'is_warranty' => $schema->boolean()->description('Whether this is a warranty maintenance'), + 'cost' => $schema->number()->description('Cost of the maintenance'), + 'start_date' => $schema->string()->description('Start date (YYYY-MM-DD, defaults to today)'), + 'completion_date' => $schema->string()->description('Completion date (YYYY-MM-DD)'), + 'notes' => $schema->string()->description('Notes about the maintenance'), + 'user_id' => $schema->number()->description('Technician user ID'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the maintenance was created'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the new maintenance record'), + 'title' => $schema->string()->description('Title of the maintenance'), + 'asset_id' => $schema->number()->description('Asset ID'), + 'asset_tag' => $schema->string()->description('Asset tag'), + ]; + } +} diff --git a/app/Mcp/Tools/CreateManufacturerTool.php b/app/Mcp/Tools/CreateManufacturerTool.php new file mode 100644 index 0000000000..77ce842979 --- /dev/null +++ b/app/Mcp/Tools/CreateManufacturerTool.php @@ -0,0 +1,83 @@ +validate([ + 'name' => 'required|string|max:255', + 'url' => 'nullable|string|max:255', + 'support_url' => 'nullable|string|max:255', + 'support_email' => 'nullable|email|max:191', + 'support_phone' => 'nullable|string|max:191', + 'warranty_lookup_url' => 'nullable|string|max:255', + 'notes' => 'nullable|string', + ]); + } catch (ValidationException $e) { + return Response::make(Response::error($e->validator->errors()->first())); + } + + $manufacturer = new Manufacturer; + $manufacturer->fill($request->only([ + 'name', 'url', 'support_url', 'support_email', 'support_phone', 'warranty_lookup_url', 'notes', + ])); + + if ($manufacturer->save()) { + return Response::make( + Response::text('Manufacturer '.$manufacturer->name.' created successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Manufacturer created successfully', + 'id' => $manufacturer->id, + 'name' => $manufacturer->name, + ]); + } + + return Response::make(Response::error('Create failed: '.$manufacturer->getErrors()->first())); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'name' => $schema->string()->description('Manufacturer name (required)'), + 'url' => $schema->string()->description('Manufacturer website URL'), + 'support_url' => $schema->string()->description('Support website URL'), + 'support_email' => $schema->string()->description('Support email address'), + 'support_phone' => $schema->string()->description('Support phone number'), + 'warranty_lookup_url' => $schema->string()->description('Warranty lookup URL'), + 'notes' => $schema->string()->description('Notes'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the manufacturer was created'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the new manufacturer'), + 'name' => $schema->string()->description('Name of the new manufacturer'), + ]; + } +} diff --git a/app/Mcp/Tools/CreateStatusLabelTool.php b/app/Mcp/Tools/CreateStatusLabelTool.php new file mode 100644 index 0000000000..2d50597cf3 --- /dev/null +++ b/app/Mcp/Tools/CreateStatusLabelTool.php @@ -0,0 +1,95 @@ +validate([ + 'name' => 'required|string|max:255', + 'type' => 'required|string|in:deployable,pending,archived,undeployable', + 'color' => 'nullable|string', + 'notes' => 'nullable|string', + 'default_label' => 'nullable|boolean', + 'show_in_nav' => 'nullable|boolean', + ]); + } catch (ValidationException $e) { + return Response::make(Response::error($e->validator->errors()->first())); + } + + $statuslabel = new Statuslabel; + $statuslabel->name = $request->get('name'); + + $statusType = Statuslabel::getStatuslabelTypesForDB($request->get('type')); + $statuslabel->deployable = $statusType['deployable']; + $statuslabel->pending = $statusType['pending']; + $statuslabel->archived = $statusType['archived']; + + if ($request->filled('color')) { + $statuslabel->color = $request->get('color'); + } + if ($request->filled('notes')) { + $statuslabel->notes = $request->get('notes'); + } + $statuslabel->default_label = $request->get('default_label', 0); + $statuslabel->show_in_nav = $request->get('show_in_nav', 0); + + if ($statuslabel->save()) { + return Response::make( + Response::text('Status label '.$statuslabel->name.' created successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Status label created successfully', + 'id' => $statuslabel->id, + 'name' => $statuslabel->name, + 'type' => $statuslabel->getStatuslabelType(), + ]); + } + + return Response::make(Response::error('Create failed: '.$statuslabel->getErrors()->first())); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'name' => $schema->string()->description('Status label name (required)'), + 'type' => $schema->string()->description('Status label type: deployable, pending, archived, or undeployable (required)'), + 'color' => $schema->string()->description('Display color in #RRGGBB format'), + 'notes' => $schema->string()->description('Notes'), + 'default_label' => $schema->boolean()->description('Whether this is the default label'), + 'show_in_nav' => $schema->boolean()->description('Whether to show in navigation'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the status label was created'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the new status label'), + 'name' => $schema->string()->description('Name of the new status label'), + 'type' => $schema->string()->description('Type of the new status label'), + ]; + } +} diff --git a/app/Mcp/Tools/CreateSupplierTool.php b/app/Mcp/Tools/CreateSupplierTool.php new file mode 100644 index 0000000000..ef4d849b04 --- /dev/null +++ b/app/Mcp/Tools/CreateSupplierTool.php @@ -0,0 +1,96 @@ +validate([ + 'name' => 'required|string|max:255', + 'address' => 'nullable|string', + 'address2' => 'nullable|string', + 'city' => 'nullable|string', + 'state' => 'nullable|string', + 'country' => 'nullable|string', + 'zip' => 'nullable|string', + 'phone' => 'nullable|string', + 'fax' => 'nullable|string', + 'email' => 'nullable|email', + 'url' => 'nullable|string', + 'contact' => 'nullable|string', + 'notes' => 'nullable|string', + ]); + } catch (ValidationException $e) { + return Response::make(Response::error($e->validator->errors()->first())); + } + + $supplier = new Supplier; + $supplier->fill($request->only([ + 'name', 'address', 'address2', 'city', 'state', 'country', 'zip', + 'phone', 'fax', 'email', 'url', 'contact', 'notes', + ])); + + if ($supplier->save()) { + return Response::make( + Response::text('Supplier '.$supplier->name.' created successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Supplier created successfully', + 'id' => $supplier->id, + 'name' => $supplier->name, + ]); + } + + return Response::make(Response::error('Create failed: '.$supplier->getErrors()->first())); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'name' => $schema->string()->description('Supplier name (required)'), + 'address' => $schema->string()->description('Address line 1'), + 'address2' => $schema->string()->description('Address line 2'), + 'city' => $schema->string()->description('City'), + 'state' => $schema->string()->description('State'), + 'country' => $schema->string()->description('Country'), + 'zip' => $schema->string()->description('Postal code'), + 'phone' => $schema->string()->description('Phone number'), + 'fax' => $schema->string()->description('Fax number'), + 'email' => $schema->string()->description('Email address'), + 'url' => $schema->string()->description('Website URL'), + 'contact' => $schema->string()->description('Contact name'), + 'notes' => $schema->string()->description('Notes'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the supplier was created'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the new supplier'), + 'name' => $schema->string()->description('Name of the new supplier'), + ]; + } +} diff --git a/app/Mcp/Tools/DeleteAssetModelTool.php b/app/Mcp/Tools/DeleteAssetModelTool.php new file mode 100644 index 0000000000..f2a5d0d6f6 --- /dev/null +++ b/app/Mcp/Tools/DeleteAssetModelTool.php @@ -0,0 +1,83 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $model = $this->resolveModel($request); + + if (! $model) { + return Response::make(Response::error('Asset model not found')); + } + + if (! Gate::allows('delete', $model)) { + return Response::make(Response::error('Unauthorized')); + } + + if ($model->assets()->count() > 0) { + return Response::make(Response::error('Model has assets and cannot be deleted')); + } + + $name = $model->name; + + $model->delete(); + + return Response::make( + Response::text('Asset model '.$name.' deleted successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Asset model deleted successfully', + 'name' => $name, + ]); + } + + private function resolveModel(Request $request): ?AssetModel + { + if ($request->filled('id')) { + return AssetModel::find($request->get('id')); + } + if ($request->filled('name')) { + return AssetModel::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the asset model to delete'), + 'name' => $schema->string()->description('Name of the asset model 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 asset model'), + ]; + } +} diff --git a/app/Mcp/Tools/DeleteCategoryTool.php b/app/Mcp/Tools/DeleteCategoryTool.php new file mode 100644 index 0000000000..a930366f5f --- /dev/null +++ b/app/Mcp/Tools/DeleteCategoryTool.php @@ -0,0 +1,83 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $category = $this->resolveCategory($request); + + if (! $category) { + return Response::make(Response::error('Category not found')); + } + + if (! Gate::allows('delete', $category)) { + return Response::make(Response::error('Unauthorized')); + } + + $name = $category->name; + + try { + $category->delete(); + } catch (\Exception $e) { + return Response::make(Response::error('Category cannot be deleted: '.$e->getMessage())); + } + + return Response::make( + Response::text('Category '.$name.' deleted successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Category deleted successfully', + 'name' => $name, + ]); + } + + private function resolveCategory(Request $request): ?Category + { + if ($request->filled('id')) { + return Category::find($request->get('id')); + } + if ($request->filled('name')) { + return Category::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the category to delete'), + 'name' => $schema->string()->description('Name of the category 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 category'), + ]; + } +} diff --git a/app/Mcp/Tools/DeleteCompanyTool.php b/app/Mcp/Tools/DeleteCompanyTool.php new file mode 100644 index 0000000000..eb7fe3ff41 --- /dev/null +++ b/app/Mcp/Tools/DeleteCompanyTool.php @@ -0,0 +1,79 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $company = $this->resolveCompany($request); + + if (! $company) { + return Response::make(Response::error('Company not found')); + } + + if (! Gate::allows('delete', $company)) { + return Response::make(Response::error('Unauthorized')); + } + + $name = $company->name; + + $company->delete(); + + return Response::make( + Response::text('Company '.$name.' deleted successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Company deleted successfully', + 'name' => $name, + ]); + } + + private function resolveCompany(Request $request): ?Company + { + if ($request->filled('id')) { + return Company::find($request->get('id')); + } + if ($request->filled('name')) { + return Company::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the company to delete'), + 'name' => $schema->string()->description('Name of the company 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 company'), + ]; + } +} diff --git a/app/Mcp/Tools/DeleteConsumableTool.php b/app/Mcp/Tools/DeleteConsumableTool.php new file mode 100644 index 0000000000..027f9d7958 --- /dev/null +++ b/app/Mcp/Tools/DeleteConsumableTool.php @@ -0,0 +1,83 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $consumable = $this->resolveConsumable($request); + + if (! $consumable) { + return Response::make(Response::error('Consumable not found')); + } + + if (! Gate::allows('delete', $consumable)) { + return Response::make(Response::error('Unauthorized')); + } + + if ($consumable->users()->count() > 0) { + return Response::make(Response::error('Consumable has items checked out and cannot be deleted')); + } + + $name = $consumable->name; + + $consumable->delete(); + + return Response::make( + Response::text('Consumable '.$name.' deleted successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Consumable deleted successfully', + 'name' => $name, + ]); + } + + private function resolveConsumable(Request $request): ?Consumable + { + if ($request->filled('id')) { + return Consumable::find($request->get('id')); + } + if ($request->filled('name')) { + return Consumable::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the consumable to delete'), + 'name' => $schema->string()->description('Name of the consumable 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 consumable'), + ]; + } +} diff --git a/app/Mcp/Tools/DeleteDepreciationTool.php b/app/Mcp/Tools/DeleteDepreciationTool.php new file mode 100644 index 0000000000..2573294f78 --- /dev/null +++ b/app/Mcp/Tools/DeleteDepreciationTool.php @@ -0,0 +1,79 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $dep = $this->resolveDepreciation($request); + + if (! $dep) { + return Response::make(Response::error('Depreciation not found')); + } + + if (! Gate::allows('delete', $dep)) { + return Response::make(Response::error('Unauthorized')); + } + + $name = $dep->name; + + $dep->delete(); + + return Response::make( + Response::text('Depreciation '.$name.' deleted successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Depreciation deleted successfully', + 'name' => $name, + ]); + } + + private function resolveDepreciation(Request $request): ?Depreciation + { + if ($request->filled('id')) { + return Depreciation::find($request->get('id')); + } + if ($request->filled('name')) { + return Depreciation::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the depreciation to delete'), + 'name' => $schema->string()->description('Name of the depreciation 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 depreciation'), + ]; + } +} diff --git a/app/Mcp/Tools/DeleteGroupTool.php b/app/Mcp/Tools/DeleteGroupTool.php new file mode 100644 index 0000000000..9674f58ae2 --- /dev/null +++ b/app/Mcp/Tools/DeleteGroupTool.php @@ -0,0 +1,75 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + if ($request->filled('id')) { + $group = Group::find($request->get('id')); + } elseif ($request->filled('name')) { + $group = Group::where('name', $request->get('name'))->first(); + } else { + return Response::make(Response::error('Please provide an id or name')); + } + + if (! $group) { + return Response::make(Response::error('Group not found')); + } + + $groupName = $group->name; + + if ($group->delete()) { + return Response::make( + Response::text('Group '.$groupName.' deleted successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Group deleted successfully', + 'name' => $groupName, + ]); + } + + return Response::make(Response::error('Delete failed')); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric group ID to delete'), + 'name' => $schema->string()->description('Group name 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 group'), + ]; + } +} diff --git a/app/Mcp/Tools/DeleteLocationTool.php b/app/Mcp/Tools/DeleteLocationTool.php new file mode 100644 index 0000000000..1fb09f3a9b --- /dev/null +++ b/app/Mcp/Tools/DeleteLocationTool.php @@ -0,0 +1,87 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $location = $this->resolveLocation($request); + + if (! $location) { + return Response::make(Response::error('Location not found')); + } + + if (! Gate::allows('delete', $location)) { + return Response::make(Response::error('Unauthorized')); + } + + if ($location->users()->count() > 0) { + return Response::make(Response::error('Location has users assigned and cannot be deleted')); + } + + if ($location->children()->count() > 0) { + return Response::make(Response::error('Location has child locations and cannot be deleted')); + } + + $name = $location->name; + + $location->delete(); + + return Response::make( + Response::text('Location '.$name.' deleted successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Location deleted successfully', + 'name' => $name, + ]); + } + + private function resolveLocation(Request $request): ?Location + { + if ($request->filled('id')) { + return Location::find($request->get('id')); + } + if ($request->filled('name')) { + return Location::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the location to delete'), + 'name' => $schema->string()->description('Name of the location 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 location'), + ]; + } +} diff --git a/app/Mcp/Tools/DeleteManufacturerTool.php b/app/Mcp/Tools/DeleteManufacturerTool.php new file mode 100644 index 0000000000..9466107b8e --- /dev/null +++ b/app/Mcp/Tools/DeleteManufacturerTool.php @@ -0,0 +1,79 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $manufacturer = $this->resolveManufacturer($request); + + if (! $manufacturer) { + return Response::make(Response::error('Manufacturer not found')); + } + + if (! Gate::allows('delete', $manufacturer)) { + return Response::make(Response::error('Unauthorized')); + } + + $name = $manufacturer->name; + + $manufacturer->delete(); + + return Response::make( + Response::text('Manufacturer '.$name.' deleted successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Manufacturer deleted successfully', + 'name' => $name, + ]); + } + + private function resolveManufacturer(Request $request): ?Manufacturer + { + if ($request->filled('id')) { + return Manufacturer::find($request->get('id')); + } + if ($request->filled('name')) { + return Manufacturer::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the manufacturer to delete'), + 'name' => $schema->string()->description('Name of the manufacturer 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 manufacturer'), + ]; + } +} diff --git a/app/Mcp/Tools/DeleteStatusLabelTool.php b/app/Mcp/Tools/DeleteStatusLabelTool.php new file mode 100644 index 0000000000..19f41daff5 --- /dev/null +++ b/app/Mcp/Tools/DeleteStatusLabelTool.php @@ -0,0 +1,83 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $label = $this->resolveStatusLabel($request); + + if (! $label) { + return Response::make(Response::error('Status label not found')); + } + + if (! Gate::allows('delete', $label)) { + return Response::make(Response::error('Unauthorized')); + } + + if ($label->assets()->count() > 0) { + return Response::make(Response::error('Status label has assets assigned and cannot be deleted')); + } + + $name = $label->name; + + $label->delete(); + + return Response::make( + Response::text('Status label '.$name.' deleted successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Status label deleted successfully', + 'name' => $name, + ]); + } + + private function resolveStatusLabel(Request $request): ?Statuslabel + { + if ($request->filled('id')) { + return Statuslabel::find($request->get('id')); + } + if ($request->filled('name')) { + return Statuslabel::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the status label to delete'), + 'name' => $schema->string()->description('Name of the status label 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 status label'), + ]; + } +} diff --git a/app/Mcp/Tools/DeleteSupplierTool.php b/app/Mcp/Tools/DeleteSupplierTool.php new file mode 100644 index 0000000000..42cf3bfd19 --- /dev/null +++ b/app/Mcp/Tools/DeleteSupplierTool.php @@ -0,0 +1,79 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $supplier = $this->resolveSupplier($request); + + if (! $supplier) { + return Response::make(Response::error('Supplier not found')); + } + + if (! Gate::allows('delete', $supplier)) { + return Response::make(Response::error('Unauthorized')); + } + + $name = $supplier->name; + + $supplier->delete(); + + return Response::make( + Response::text('Supplier '.$name.' deleted successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Supplier deleted successfully', + 'name' => $name, + ]); + } + + private function resolveSupplier(Request $request): ?Supplier + { + if ($request->filled('id')) { + return Supplier::find($request->get('id')); + } + if ($request->filled('name')) { + return Supplier::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the supplier to delete'), + 'name' => $schema->string()->description('Name of the supplier 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 supplier'), + ]; + } +} diff --git a/app/Mcp/Tools/GetActivityLogTool.php b/app/Mcp/Tools/GetActivityLogTool.php new file mode 100644 index 0000000000..d15baeb858 --- /dev/null +++ b/app/Mcp/Tools/GetActivityLogTool.php @@ -0,0 +1,102 @@ +validate([ + 'item_type' => 'nullable|string|max:255', + 'item_id' => 'nullable|integer', + 'user_id' => 'nullable|integer', + 'action_type' => 'nullable|string|max:255', + 'limit' => 'nullable|integer|min:1|max:500', + 'offset' => 'nullable|integer|min:0', + ]); + + $logs = Actionlog::with('user', 'item')->orderBy('created_at', 'desc'); + + if ($request->filled('item_type')) { + $logs->where('item_type', $request->get('item_type')); + } + + if ($request->filled('item_id')) { + $logs->where('item_id', $request->get('item_id')); + } + + if ($request->filled('user_id')) { + $logs->where('user_id', $request->get('user_id')); + } + + if ($request->filled('action_type')) { + $logs->where('action_type', $request->get('action_type')); + } + + $total = $logs->count(); + $limit = $request->filled('limit') ? (int) $request->get('limit') : 25; + $offset = $request->filled('offset') ? (int) $request->get('offset') : 0; + + $results = $logs->skip($offset)->take($limit)->get(); + + $activityData = $results->map(fn (Actionlog $log) => [ + 'id' => $log->id, + 'action_type' => $log->action_type, + 'item_type' => $log->item_type, + 'item_id' => $log->item_id, + 'user_id' => $log->user_id, + 'user' => $log->user?->username, + 'note' => $log->note, + 'created_at' => $log->created_at?->toDateTimeString(), + ])->values()->all(); + + return Response::make( + Response::text("Found {$total} activity log entries, returning ".count($activityData)) + )->withStructuredContent([ + 'total' => $total, + 'offset' => $offset, + 'limit' => $limit, + 'activity' => $activityData, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'item_type' => $schema->string()->description('Filter by item type (e.g. App\\Models\\Asset)'), + 'item_id' => $schema->number()->description('Filter by item ID'), + 'user_id' => $schema->number()->description('Filter by user ID'), + 'action_type' => $schema->string()->description('Filter by action type (e.g. checkout, checkin, update)'), + '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 log entries')->required(), + 'offset' => $schema->number()->description('Current pagination offset')->required(), + 'limit' => $schema->number()->description('Results per page')->required(), + 'activity' => $schema->array()->description('List of activity log entries')->required(), + ]; + } +} diff --git a/app/Mcp/Tools/GetCurrentUserTool.php b/app/Mcp/Tools/GetCurrentUserTool.php new file mode 100644 index 0000000000..dac5fecd75 --- /dev/null +++ b/app/Mcp/Tools/GetCurrentUserTool.php @@ -0,0 +1,72 @@ +check()) { + return Response::make(Response::error('Not authenticated')); + } + + $user = User::with('company', 'department', 'userloc')->find(auth()->id()); + + if (! $user) { + return Response::make(Response::error('Not authenticated')); + } + + return Response::make( + Response::text('Current user: '.$user->username) + )->withStructuredContent([ + 'id' => $user->id, + 'username' => $user->username, + 'first_name' => $user->first_name, + 'last_name' => $user->last_name, + 'email' => $user->email, + 'company' => $user->company?->name, + 'department' => $user->department?->name, + 'location' => $user->userloc?->name, + 'employee_num' => $user->employee_num, + 'title' => $user->jobtitle, + 'phone' => $user->phone, + 'activated' => (bool) $user->activated, + ]); + } + + public function schema(JsonSchema $schema): array + { + return []; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric user ID')->required(), + 'username' => $schema->string()->description('Username')->required(), + 'first_name' => $schema->string()->description('First name'), + 'last_name' => $schema->string()->description('Last name'), + 'email' => $schema->string()->description('Email address'), + 'company' => $schema->string()->description('Company name'), + 'department' => $schema->string()->description('Department name'), + 'location' => $schema->string()->description('Default location name'), + 'employee_num' => $schema->string()->description('Employee number'), + 'title' => $schema->string()->description('Job title'), + 'phone' => $schema->string()->description('Phone number'), + 'activated' => $schema->boolean()->description('Whether the account is activated'), + ]; + } +} diff --git a/app/Mcp/Tools/GetUserAssetsTool.php b/app/Mcp/Tools/GetUserAssetsTool.php new file mode 100644 index 0000000000..3e5eea9cf7 --- /dev/null +++ b/app/Mcp/Tools/GetUserAssetsTool.php @@ -0,0 +1,82 @@ +validate([ + 'id' => 'required|integer', + ]); + + $user = User::find($request->get('id')); + + if (! $user) { + return Response::make(Response::error('User not found')); + } + + $assets = Asset::where('assigned_to', $user->id) + ->where('assigned_type', User::class) + ->with('model', 'status', 'location') + ->get(); + + $data = $assets->map(fn ($asset) => [ + 'id' => $asset->id, + 'asset_tag' => $asset->asset_tag, + 'name' => $asset->name, + 'serial' => $asset->serial, + 'model' => $asset->model?->name, + 'status' => $asset->status?->name, + ])->values()->all(); + + return Response::make( + Response::text('Found '.count($data).' assets for user '.$user->username) + )->withStructuredContent([ + 'user_id' => $user->id, + 'username' => $user->username, + 'total' => count($data), + 'assets' => $data, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the user whose assets should be listed (required)'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'user_id' => $schema->number()->description('Numeric ID of the user')->required(), + 'username' => $schema->string()->description('Username of the user')->required(), + 'total' => $schema->number()->description('Total number of assets checked out to the user')->required(), + 'assets' => $schema->array()->description('List of checked-out assets'), + ]; + } +} diff --git a/app/Mcp/Tools/ListAssetModelsTool.php b/app/Mcp/Tools/ListAssetModelsTool.php new file mode 100644 index 0000000000..289e548208 --- /dev/null +++ b/app/Mcp/Tools/ListAssetModelsTool.php @@ -0,0 +1,101 @@ +validate([ + 'search' => 'nullable|string|max:255', + 'category_id' => 'nullable|integer', + 'manufacturer_id' => 'nullable|integer', + 'limit' => 'nullable|integer|min:1|max:500', + 'offset' => 'nullable|integer|min:0', + ]); + + $models = AssetModel::with('category', 'manufacturer', 'depreciation') + ->withCount('assets as assets_count'); + + if ($request->filled('search')) { + $models->TextSearch($request->get('search')); + } + + if ($request->filled('category_id')) { + $models->where('category_id', $request->get('category_id')); + } + + if ($request->filled('manufacturer_id')) { + $models->where('manufacturer_id', $request->get('manufacturer_id')); + } + + $models->orderBy('models.created_at', 'desc'); + + $total = $models->count(); + $limit = $request->filled('limit') ? (int) $request->get('limit') : 25; + $offset = $request->filled('offset') ? (int) $request->get('offset') : 0; + + $results = $models->skip($offset)->take($limit)->get(); + + $modelsData = $results->map(fn (AssetModel $model) => [ + 'id' => $model->id, + 'name' => $model->name, + 'model_number' => $model->model_number, + 'category_id' => $model->category_id, + 'category' => $model->category?->name, + 'manufacturer_id' => $model->manufacturer_id, + 'manufacturer' => $model->manufacturer?->name, + 'assets_count' => $model->assets_count, + 'eol' => $model->eol, + 'min_amt' => $model->min_amt, + ])->values()->all(); + + return Response::make( + Response::text("Found {$total} asset models, returning ".count($modelsData)) + )->withStructuredContent([ + 'total' => $total, + 'offset' => $offset, + 'limit' => $limit, + 'models' => $modelsData, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'search' => $schema->string()->description('Keyword to search across model name, model number'), + 'category_id' => $schema->number()->description('Filter by category ID'), + 'manufacturer_id' => $schema->number()->description('Filter by manufacturer 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 asset models')->required(), + 'offset' => $schema->number()->description('Current pagination offset')->required(), + 'limit' => $schema->number()->description('Results per page')->required(), + 'models' => $schema->array()->description('List of asset models')->required(), + ]; + } +} diff --git a/app/Mcp/Tools/ListCategoriesTool.php b/app/Mcp/Tools/ListCategoriesTool.php new file mode 100644 index 0000000000..8638c700ad --- /dev/null +++ b/app/Mcp/Tools/ListCategoriesTool.php @@ -0,0 +1,98 @@ +validate([ + 'search' => 'nullable|string|max:255', + 'category_type' => 'nullable|string|in:asset,accessory,consumable,component,license', + 'limit' => 'nullable|integer|min:1|max:500', + 'offset' => 'nullable|integer|min:0', + ]); + + $categories = Category::withCount( + 'showableAssets as assets_count', + 'accessories as accessories_count', + 'consumables as consumables_count', + 'components as components_count', + 'licenses as licenses_count' + ); + + if ($request->filled('search')) { + $categories->TextSearch($request->get('search')); + } + + if ($request->filled('category_type')) { + $categories->where('category_type', $request->get('category_type')); + } + + $categories->orderBy('created_at', 'desc'); + + $total = $categories->count(); + $limit = $request->filled('limit') ? (int) $request->get('limit') : 25; + $offset = $request->filled('offset') ? (int) $request->get('offset') : 0; + + $results = $categories->skip($offset)->take($limit)->get(); + + $categoriesData = $results->map(fn (Category $category) => [ + 'id' => $category->id, + 'name' => $category->name, + 'category_type' => $category->category_type, + 'assets_count' => $category->assets_count, + 'accessories_count' => $category->accessories_count, + 'consumables_count' => $category->consumables_count, + 'components_count' => $category->components_count, + 'licenses_count' => $category->licenses_count, + ])->values()->all(); + + return Response::make( + Response::text("Found {$total} categories, returning ".count($categoriesData)) + )->withStructuredContent([ + 'total' => $total, + 'offset' => $offset, + 'limit' => $limit, + 'categories' => $categoriesData, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'search' => $schema->string()->description('Keyword to search across category name, type, notes'), + 'category_type' => $schema->string()->description('Filter by type: asset, accessory, consumable, component, or license'), + '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 categories')->required(), + 'offset' => $schema->number()->description('Current pagination offset')->required(), + 'limit' => $schema->number()->description('Results per page')->required(), + 'categories' => $schema->array()->description('List of categories')->required(), + ]; + } +} diff --git a/app/Mcp/Tools/ListCompaniesTool.php b/app/Mcp/Tools/ListCompaniesTool.php new file mode 100644 index 0000000000..27acb91d53 --- /dev/null +++ b/app/Mcp/Tools/ListCompaniesTool.php @@ -0,0 +1,88 @@ +validate([ + 'search' => 'nullable|string|max:255', + 'limit' => 'nullable|integer|min:1|max:500', + 'offset' => 'nullable|integer|min:0', + ]); + + $companies = Company::withCount([ + 'assets as assets_count' => fn ($q) => $q->AssetsForShow(), + ])->withCount('licenses as licenses_count', 'users as users_count'); + + if ($request->filled('search')) { + $companies->TextSearch($request->get('search')); + } + + $companies->orderBy('created_at', 'desc'); + + $total = $companies->count(); + $limit = $request->filled('limit') ? (int) $request->get('limit') : 25; + $offset = $request->filled('offset') ? (int) $request->get('offset') : 0; + + $results = $companies->skip($offset)->take($limit)->get(); + + $companiesData = $results->map(fn (Company $company) => [ + 'id' => $company->id, + 'name' => $company->name, + 'phone' => $company->phone, + 'fax' => $company->fax, + 'email' => $company->email, + 'assets_count' => $company->assets_count, + 'licenses_count' => $company->licenses_count, + 'users_count' => $company->users_count, + ])->values()->all(); + + return Response::make( + Response::text("Found {$total} companies, returning ".count($companiesData)) + )->withStructuredContent([ + 'total' => $total, + 'offset' => $offset, + 'limit' => $limit, + 'companies' => $companiesData, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'search' => $schema->string()->description('Keyword to search across company name, phone, fax, email'), + '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 companies')->required(), + 'offset' => $schema->number()->description('Current pagination offset')->required(), + 'limit' => $schema->number()->description('Results per page')->required(), + 'companies' => $schema->array()->description('List of companies')->required(), + ]; + } +} diff --git a/app/Mcp/Tools/ListConsumablesTool.php b/app/Mcp/Tools/ListConsumablesTool.php new file mode 100644 index 0000000000..192a37e3d9 --- /dev/null +++ b/app/Mcp/Tools/ListConsumablesTool.php @@ -0,0 +1,110 @@ +validate([ + 'search' => 'nullable|string|max:255', + 'company_id' => 'nullable|integer', + 'category_id' => 'nullable|integer', + 'manufacturer_id' => 'nullable|integer', + 'location_id' => 'nullable|integer', + 'limit' => 'nullable|integer|min:1|max:500', + 'offset' => 'nullable|integer|min:0', + ]); + + $consumables = Consumable::with('company', 'category', 'manufacturer', 'supplier', 'location') + ->withCount('users as users_count'); + + if ($request->filled('search')) { + $consumables->TextSearch($request->get('search')); + } + + if ($request->filled('company_id')) { + $consumables->where('consumables.company_id', $request->get('company_id')); + } + + if ($request->filled('category_id')) { + $consumables->where('category_id', $request->get('category_id')); + } + + if ($request->filled('manufacturer_id')) { + $consumables->where('manufacturer_id', $request->get('manufacturer_id')); + } + + if ($request->filled('location_id')) { + $consumables->where('location_id', $request->get('location_id')); + } + + $total = $consumables->count(); + $limit = $request->filled('limit') ? (int) $request->get('limit') : 25; + $offset = $request->filled('offset') ? (int) $request->get('offset') : 0; + + $results = $consumables->orderBy('consumables.created_at', 'desc')->skip($offset)->take($limit)->get(); + + $consumablesData = $results->map(fn (Consumable $consumable) => [ + 'id' => $consumable->id, + 'name' => $consumable->name, + 'qty' => $consumable->qty, + 'users_count' => $consumable->users_count, + 'category' => $consumable->category?->name, + 'manufacturer' => $consumable->manufacturer?->name, + 'company' => $consumable->company?->name, + 'location' => $consumable->location?->name, + 'purchase_cost' => $consumable->purchase_cost, + 'purchase_date' => $consumable->purchase_date?->format('Y-m-d'), + ])->values()->all(); + + return Response::make( + Response::text("Found {$total} consumables, returning ".count($consumablesData)) + )->withStructuredContent([ + 'total' => $total, + 'offset' => $offset, + 'limit' => $limit, + 'consumables' => $consumablesData, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'search' => $schema->string()->description('Keyword to search across consumable name and other fields'), + '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'), + 'location_id' => $schema->number()->description('Filter by location 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 consumables')->required(), + 'offset' => $schema->number()->description('Current pagination offset')->required(), + 'limit' => $schema->number()->description('Results per page')->required(), + ]; + } +} diff --git a/app/Mcp/Tools/ListDepreciationsTool.php b/app/Mcp/Tools/ListDepreciationsTool.php new file mode 100644 index 0000000000..d784b25627 --- /dev/null +++ b/app/Mcp/Tools/ListDepreciationsTool.php @@ -0,0 +1,82 @@ +validate([ + 'search' => 'nullable|string|max:255', + 'limit' => 'nullable|integer|min:1|max:500', + 'offset' => 'nullable|integer|min:0', + ]); + + $depreciations = Depreciation::withCount('models as models_count'); + + if ($request->filled('search')) { + $depreciations->TextSearch($request->get('search')); + } + + $depreciations->orderBy('created_at', 'desc'); + + $total = $depreciations->count(); + $limit = $request->filled('limit') ? (int) $request->get('limit') : 25; + $offset = $request->filled('offset') ? (int) $request->get('offset') : 0; + + $results = $depreciations->skip($offset)->take($limit)->get(); + + $depreciationsData = $results->map(fn (Depreciation $dep) => [ + 'id' => $dep->id, + 'name' => $dep->name, + 'months' => $dep->months, + 'models_count' => $dep->models_count, + ])->values()->all(); + + return Response::make( + Response::text("Found {$total} depreciations, returning ".count($depreciationsData)) + )->withStructuredContent([ + 'total' => $total, + 'offset' => $offset, + 'limit' => $limit, + 'depreciations' => $depreciationsData, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'search' => $schema->string()->description('Keyword to search across depreciation name'), + '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 depreciations')->required(), + 'offset' => $schema->number()->description('Current pagination offset')->required(), + 'limit' => $schema->number()->description('Results per page')->required(), + 'depreciations' => $schema->array()->description('List of depreciation schedules')->required(), + ]; + } +} diff --git a/app/Mcp/Tools/ListGroupsTool.php b/app/Mcp/Tools/ListGroupsTool.php new file mode 100644 index 0000000000..80a4783327 --- /dev/null +++ b/app/Mcp/Tools/ListGroupsTool.php @@ -0,0 +1,81 @@ +validate([ + 'search' => 'nullable|string|max:255', + 'limit' => 'nullable|integer|min:1|max:500', + 'offset' => 'nullable|integer|min:0', + ]); + + $groups = Group::withCount('users as users_count') + ->orderBy('created_at', 'desc'); + + if ($request->filled('search')) { + $groups->TextSearch($request->get('search')); + } + + $total = $groups->count(); + $limit = $request->filled('limit') ? (int) $request->get('limit') : 25; + $offset = $request->filled('offset') ? (int) $request->get('offset') : 0; + + $results = $groups->skip($offset)->take($limit)->get(); + + $groupsData = $results->map(fn (Group $group) => [ + 'id' => $group->id, + 'name' => $group->name, + 'notes' => $group->notes, + 'users_count' => $group->users_count, + ])->values()->all(); + + return Response::make( + Response::text("Found {$total} groups, returning ".count($groupsData)) + )->withStructuredContent([ + 'total' => $total, + 'offset' => $offset, + 'limit' => $limit, + 'groups' => $groupsData, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'search' => $schema->string()->description('Keyword to search groups by name or notes'), + '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 groups')->required(), + 'offset' => $schema->number()->description('Current pagination offset')->required(), + 'limit' => $schema->number()->description('Results per page')->required(), + 'groups' => $schema->array()->description('List of groups')->required(), + ]; + } +} diff --git a/app/Mcp/Tools/ListLocationsTool.php b/app/Mcp/Tools/ListLocationsTool.php new file mode 100644 index 0000000000..d5d452e80d --- /dev/null +++ b/app/Mcp/Tools/ListLocationsTool.php @@ -0,0 +1,101 @@ +validate([ + 'search' => 'nullable|string|max:255', + 'parent_id' => 'nullable|integer', + 'limit' => 'nullable|integer|min:1|max:500', + 'offset' => 'nullable|integer|min:0', + ]); + + $locations = Location::with('parent')->withCount( + 'assets as assets_count', + 'users as users_count', + 'children as children_count' + ); + + if ($request->filled('search')) { + $locations->TextSearch($request->get('search')); + } + + if ($request->filled('parent_id')) { + $locations->where('parent_id', $request->get('parent_id')); + } + + $locations->orderBy('created_at', 'desc'); + + $total = $locations->count(); + $limit = $request->filled('limit') ? (int) $request->get('limit') : 25; + $offset = $request->filled('offset') ? (int) $request->get('offset') : 0; + + $results = $locations->skip($offset)->take($limit)->get(); + + $locationsData = $results->map(fn (Location $location) => [ + 'id' => $location->id, + 'name' => $location->name, + 'address' => $location->address, + 'city' => $location->city, + 'state' => $location->state, + 'country' => $location->country, + 'zip' => $location->zip, + 'phone' => $location->phone, + 'parent_id' => $location->parent_id, + 'parent' => $location->parent?->name, + 'assets_count' => $location->assets_count, + 'users_count' => $location->users_count, + 'children_count' => $location->children_count, + ])->values()->all(); + + return Response::make( + Response::text("Found {$total} locations, returning ".count($locationsData)) + )->withStructuredContent([ + 'total' => $total, + 'offset' => $offset, + 'limit' => $limit, + 'locations' => $locationsData, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'search' => $schema->string()->description('Keyword to search across location name, city, state, country'), + 'parent_id' => $schema->number()->description('Filter by parent location 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 locations')->required(), + 'offset' => $schema->number()->description('Current pagination offset')->required(), + 'limit' => $schema->number()->description('Results per page')->required(), + 'locations' => $schema->array()->description('List of locations')->required(), + ]; + } +} diff --git a/app/Mcp/Tools/ListMaintenancesTool.php b/app/Mcp/Tools/ListMaintenancesTool.php new file mode 100644 index 0000000000..ac1243a145 --- /dev/null +++ b/app/Mcp/Tools/ListMaintenancesTool.php @@ -0,0 +1,89 @@ +validate([ + 'asset_id' => 'nullable|integer', + 'limit' => 'nullable|integer|min:1|max:500', + 'offset' => 'nullable|integer|min:0', + ]); + + $maintenances = Maintenance::with('asset', 'supplier'); + + if ($request->filled('asset_id')) { + $maintenances->where('asset_id', $request->get('asset_id')); + } + + $maintenances->orderBy('created_at', 'desc'); + + $total = $maintenances->count(); + $limit = $request->filled('limit') ? (int) $request->get('limit') : 25; + $offset = $request->filled('offset') ? (int) $request->get('offset') : 0; + + $results = $maintenances->skip($offset)->take($limit)->get(); + + $maintenancesData = $results->map(fn (Maintenance $maintenance) => [ + 'id' => $maintenance->id, + 'title' => $maintenance->name, + 'asset_id' => $maintenance->asset_id, + 'asset_tag' => $maintenance->asset?->asset_tag, + 'is_warranty' => (bool) $maintenance->is_warranty, + 'cost' => $maintenance->cost, + 'start_date' => $maintenance->start_date, + 'completion_date' => $maintenance->completion_date, + 'supplier' => $maintenance->supplier?->name, + 'notes' => $maintenance->notes, + ])->values()->all(); + + return Response::make( + Response::text("Found {$total} maintenances, returning ".count($maintenancesData)) + )->withStructuredContent([ + 'total' => $total, + 'offset' => $offset, + 'limit' => $limit, + 'maintenances' => $maintenancesData, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'asset_id' => $schema->number()->description('Filter by asset 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 maintenances')->required(), + 'offset' => $schema->number()->description('Current pagination offset')->required(), + 'limit' => $schema->number()->description('Results per page')->required(), + 'maintenances' => $schema->array()->description('List of maintenances')->required(), + ]; + } +} diff --git a/app/Mcp/Tools/ListManufacturersTool.php b/app/Mcp/Tools/ListManufacturersTool.php new file mode 100644 index 0000000000..139781e3c1 --- /dev/null +++ b/app/Mcp/Tools/ListManufacturersTool.php @@ -0,0 +1,93 @@ +validate([ + 'search' => 'nullable|string|max:255', + 'limit' => 'nullable|integer|min:1|max:500', + 'offset' => 'nullable|integer|min:0', + ]); + + $manufacturers = Manufacturer::withCount( + 'assets as assets_count', + 'licenses as licenses_count', + 'accessories as accessories_count', + 'components as components_count' + ); + + if ($request->filled('search')) { + $manufacturers->TextSearch($request->get('search')); + } + + $manufacturers->orderBy('created_at', 'desc'); + + $total = $manufacturers->count(); + $limit = $request->filled('limit') ? (int) $request->get('limit') : 25; + $offset = $request->filled('offset') ? (int) $request->get('offset') : 0; + + $results = $manufacturers->skip($offset)->take($limit)->get(); + + $manufacturersData = $results->map(fn (Manufacturer $manufacturer) => [ + 'id' => $manufacturer->id, + 'name' => $manufacturer->name, + 'url' => $manufacturer->url, + 'support_url' => $manufacturer->support_url, + 'support_email' => $manufacturer->support_email, + 'support_phone' => $manufacturer->support_phone, + 'assets_count' => $manufacturer->assets_count, + 'licenses_count' => $manufacturer->licenses_count, + 'accessories_count' => $manufacturer->accessories_count, + 'components_count' => $manufacturer->components_count, + ])->values()->all(); + + return Response::make( + Response::text("Found {$total} manufacturers, returning ".count($manufacturersData)) + )->withStructuredContent([ + 'total' => $total, + 'offset' => $offset, + 'limit' => $limit, + 'manufacturers' => $manufacturersData, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'search' => $schema->string()->description('Keyword to search across manufacturer name and notes'), + '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 manufacturers')->required(), + 'offset' => $schema->number()->description('Current pagination offset')->required(), + 'limit' => $schema->number()->description('Results per page')->required(), + 'manufacturers' => $schema->array()->description('List of manufacturers')->required(), + ]; + } +} diff --git a/app/Mcp/Tools/ListStatusLabelsTool.php b/app/Mcp/Tools/ListStatusLabelsTool.php new file mode 100644 index 0000000000..4fe247f727 --- /dev/null +++ b/app/Mcp/Tools/ListStatusLabelsTool.php @@ -0,0 +1,99 @@ +validate([ + 'search' => 'nullable|string|max:255', + 'status_type' => 'nullable|string|in:deployable,pending,archived,undeployable', + 'limit' => 'nullable|integer|min:1|max:500', + 'offset' => 'nullable|integer|min:0', + ]); + + $labels = Statuslabel::withCount('assets as assets_count'); + + if ($request->filled('search')) { + $labels->TextSearch($request->get('search')); + } + + if ($request->filled('status_type')) { + $type = $request->get('status_type'); + if ($type === 'deployable') { + $labels->Deployable(); + } elseif ($type === 'pending') { + $labels->Pending(); + } elseif ($type === 'archived') { + $labels->Archived(); + } elseif ($type === 'undeployable') { + $labels->Undeployable(); + } + } + + $total = $labels->count(); + $limit = $request->filled('limit') ? (int) $request->get('limit') : 25; + $offset = $request->filled('offset') ? (int) $request->get('offset') : 0; + + $results = $labels->skip($offset)->take($limit)->get(); + + $labelsData = $results->map(fn (Statuslabel $label) => [ + 'id' => $label->id, + 'name' => $label->name, + 'type' => $label->getStatuslabelType(), + 'color' => $label->color, + 'assets_count' => $label->assets_count, + 'deployable' => $label->deployable, + 'pending' => $label->pending, + 'archived' => $label->archived, + ])->values()->all(); + + return Response::make( + Response::text("Found {$total} status labels, returning ".count($labelsData)) + )->withStructuredContent([ + 'total' => $total, + 'offset' => $offset, + 'limit' => $limit, + 'status_labels' => $labelsData, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'search' => $schema->string()->description('Keyword to search across status label name and notes'), + 'status_type' => $schema->string()->description('Filter by type: deployable, pending, archived, undeployable'), + '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 status labels')->required(), + 'offset' => $schema->number()->description('Current pagination offset')->required(), + 'limit' => $schema->number()->description('Results per page')->required(), + 'status_labels' => $schema->array()->description('List of status labels')->required(), + ]; + } +} diff --git a/app/Mcp/Tools/ListSuppliersTool.php b/app/Mcp/Tools/ListSuppliersTool.php new file mode 100644 index 0000000000..d2f75b1c0d --- /dev/null +++ b/app/Mcp/Tools/ListSuppliersTool.php @@ -0,0 +1,92 @@ +validate([ + 'search' => 'nullable|string|max:255', + 'limit' => 'nullable|integer|min:1|max:500', + 'offset' => 'nullable|integer|min:0', + ]); + + $suppliers = Supplier::withCount( + 'assets as assets_count', + 'licenses as licenses_count' + ); + + if ($request->filled('search')) { + $suppliers->TextSearch($request->get('search')); + } + + $suppliers->orderBy('created_at', 'desc'); + + $total = $suppliers->count(); + $limit = $request->filled('limit') ? (int) $request->get('limit') : 25; + $offset = $request->filled('offset') ? (int) $request->get('offset') : 0; + + $results = $suppliers->skip($offset)->take($limit)->get(); + + $suppliersData = $results->map(fn (Supplier $supplier) => [ + 'id' => $supplier->id, + 'name' => $supplier->name, + 'address' => $supplier->address, + 'city' => $supplier->city, + 'state' => $supplier->state, + 'country' => $supplier->country, + 'phone' => $supplier->phone, + 'email' => $supplier->email, + 'url' => $supplier->url, + 'assets_count' => $supplier->assets_count, + 'licenses_count' => $supplier->licenses_count, + ])->values()->all(); + + return Response::make( + Response::text("Found {$total} suppliers, returning ".count($suppliersData)) + )->withStructuredContent([ + 'total' => $total, + 'offset' => $offset, + 'limit' => $limit, + 'suppliers' => $suppliersData, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'search' => $schema->string()->description('Keyword to search across supplier fields'), + '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 suppliers')->required(), + 'offset' => $schema->number()->description('Current pagination offset')->required(), + 'limit' => $schema->number()->description('Results per page')->required(), + 'suppliers' => $schema->array()->description('List of suppliers')->required(), + ]; + } +} diff --git a/app/Mcp/Tools/Reset2FATool.php b/app/Mcp/Tools/Reset2FATool.php new file mode 100644 index 0000000000..6cecd54ba5 --- /dev/null +++ b/app/Mcp/Tools/Reset2FATool.php @@ -0,0 +1,68 @@ +validate([ + 'id' => 'required|integer', + ]); + + $user = User::find($request->get('id')); + + if (! $user) { + return Response::make(Response::error('User not found')); + } + + $user->two_factor_secret = null; + $user->two_factor_enrolled = 0; + $user->two_factor_optin = 0; + $user->save(); + + return Response::make( + Response::text('Two-factor authentication reset for '.$user->username) + )->withStructuredContent([ + 'success' => true, + 'message' => 'Two-factor authentication reset successfully', + 'id' => $user->id, + 'username' => $user->username, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the user whose 2FA should be reset (required)'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the reset succeeded'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the user'), + 'username' => $schema->string()->description('Username of the user'), + ]; + } +} diff --git a/app/Mcp/Tools/RestoreAssetTool.php b/app/Mcp/Tools/RestoreAssetTool.php new file mode 100644 index 0000000000..8a3a5c780d --- /dev/null +++ b/app/Mcp/Tools/RestoreAssetTool.php @@ -0,0 +1,69 @@ +validate([ + 'id' => 'required|integer', + ]); + + $asset = Asset::withTrashed()->find($request->get('id')); + + if (! $asset) { + return Response::make(Response::error('Asset not found')); + } + + if (! $asset->deleted_at) { + return Response::make(Response::error('Asset is not deleted')); + } + + if (! Gate::allows('delete', Asset::class)) { + return Response::make(Response::error('Unauthorized')); + } + + $asset->restore(); + + return Response::make( + Response::text('Asset '.$asset->asset_tag.' restored successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Asset restored successfully', + 'id' => $asset->id, + 'asset_tag' => $asset->asset_tag, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the asset to restore (required)'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the restore succeeded'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the restored asset'), + 'asset_tag' => $schema->string()->description('Asset tag of the restored asset'), + ]; + } +} diff --git a/app/Mcp/Tools/RestoreUserTool.php b/app/Mcp/Tools/RestoreUserTool.php new file mode 100644 index 0000000000..4bf2a2e119 --- /dev/null +++ b/app/Mcp/Tools/RestoreUserTool.php @@ -0,0 +1,73 @@ +validate([ + 'id' => 'required|integer', + ]); + + $user = User::withTrashed()->find($request->get('id')); + + if (! $user) { + return Response::make(Response::error('User not found')); + } + + if (! $user->deleted_at) { + return Response::make(Response::error('User is not deleted')); + } + + if (! Gate::allows('delete', User::class)) { + return Response::make(Response::error('Unauthorized')); + } + + $user->restore(); + + return Response::make( + Response::text('User '.$user->username.' restored successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'User restored successfully', + 'id' => $user->id, + 'username' => $user->username, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the user to restore (required)'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'success' => $schema->boolean()->description('True if the restore succeeded'), + 'message' => $schema->string()->description('Human-readable result message')->required(), + 'id' => $schema->number()->description('Numeric ID of the restored user'), + 'username' => $schema->string()->description('Username of the restored user'), + ]; + } +} diff --git a/app/Mcp/Tools/ShowAssetModelTool.php b/app/Mcp/Tools/ShowAssetModelTool.php new file mode 100644 index 0000000000..2e3b9c24f3 --- /dev/null +++ b/app/Mcp/Tools/ShowAssetModelTool.php @@ -0,0 +1,105 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $model = $this->resolveModel($request); + + if ($model === false) { + return Response::make(Response::error('Please provide an id or name')); + } + + if (! $model) { + return Response::make(Response::error('Asset model not found')); + } + + if (! Gate::allows('view', $model)) { + return Response::make(Response::error('Unauthorized')); + } + + $model->loadCount('assets as assets_count'); + + return Response::make( + Response::text('Asset model '.$model->name.' found') + )->withStructuredContent([ + 'id' => $model->id, + 'name' => $model->name, + 'model_number' => $model->model_number, + 'category_id' => $model->category_id, + 'category' => $model->category?->name, + 'manufacturer_id' => $model->manufacturer_id, + 'manufacturer' => $model->manufacturer?->name, + 'depreciation_id' => $model->depreciation_id, + 'depreciation' => $model->depreciation?->name, + 'assets_count' => $model->assets_count, + 'eol' => $model->eol, + 'min_amt' => $model->min_amt, + 'notes' => $model->notes, + 'created_at' => $model->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $model->updated_at?->format('Y-m-d H:i:s'), + ]); + } + + private function resolveModel(Request $request): AssetModel|false|null + { + if ($request->filled('id')) { + return AssetModel::with('category', 'manufacturer', 'depreciation')->find($request->get('id')); + } + if ($request->filled('name')) { + return AssetModel::with('category', 'manufacturer', 'depreciation')->where('name', $request->get('name'))->first(); + } + + return false; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the asset model to look up'), + 'name' => $schema->string()->description('Name of the asset model to look up'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric asset model ID'), + 'name' => $schema->string()->description('Asset model name'), + 'model_number' => $schema->string()->description('Model number'), + 'category_id' => $schema->number()->description('Category ID'), + 'category' => $schema->string()->description('Category name'), + 'manufacturer_id' => $schema->number()->description('Manufacturer ID'), + 'manufacturer' => $schema->string()->description('Manufacturer name'), + 'depreciation_id' => $schema->number()->description('Depreciation schedule ID'), + 'depreciation' => $schema->string()->description('Depreciation schedule name'), + 'assets_count' => $schema->number()->description('Number of assets using this model'), + 'eol' => $schema->number()->description('End of life in months'), + 'min_amt' => $schema->number()->description('Minimum quantity alert threshold'), + 'notes' => $schema->string()->description('Notes'), + 'created_at' => $schema->string()->description('Creation timestamp'), + 'updated_at' => $schema->string()->description('Last update timestamp'), + ]; + } +} diff --git a/app/Mcp/Tools/ShowCategoryTool.php b/app/Mcp/Tools/ShowCategoryTool.php new file mode 100644 index 0000000000..596c28c754 --- /dev/null +++ b/app/Mcp/Tools/ShowCategoryTool.php @@ -0,0 +1,95 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $withCounts = [ + 'showableAssets as assets_count', + 'accessories as accessories_count', + 'consumables as consumables_count', + 'components as components_count', + 'licenses as licenses_count', + ]; + + $category = null; + + if ($request->filled('id')) { + $category = Category::withCount($withCounts)->find($request->get('id')); + } elseif ($request->filled('name')) { + $category = Category::withCount($withCounts)->where('name', $request->get('name'))->first(); + } else { + return Response::make(Response::error('Please provide an id or name')); + } + + if (! $category) { + return Response::make(Response::error('Category not found')); + } + + if (! Gate::allows('view', $category)) { + return Response::make(Response::error('Unauthorized')); + } + + return Response::make( + Response::text('Category '.$category->name.' found') + )->withStructuredContent([ + 'id' => $category->id, + 'name' => $category->name, + 'category_type' => $category->category_type, + 'assets_count' => $category->assets_count, + 'accessories_count' => $category->accessories_count, + 'consumables_count' => $category->consumables_count, + 'components_count' => $category->components_count, + 'licenses_count' => $category->licenses_count, + 'notes' => $category->notes, + 'created_at' => $category->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $category->updated_at?->format('Y-m-d H:i:s'), + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the category to look up'), + 'name' => $schema->string()->description('Name of the category to look up'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric category ID'), + 'name' => $schema->string()->description('Category name'), + 'category_type' => $schema->string()->description('Category type: asset, accessory, consumable, component, or license'), + 'assets_count' => $schema->number()->description('Number of assets in this category'), + 'accessories_count' => $schema->number()->description('Number of accessories in this category'), + 'consumables_count' => $schema->number()->description('Number of consumables in this category'), + 'components_count' => $schema->number()->description('Number of components in this category'), + 'licenses_count' => $schema->number()->description('Number of licenses in this category'), + 'notes' => $schema->string()->description('Notes'), + 'created_at' => $schema->string()->description('Creation timestamp'), + 'updated_at' => $schema->string()->description('Last update timestamp'), + ]; + } +} diff --git a/app/Mcp/Tools/ShowCompanyTool.php b/app/Mcp/Tools/ShowCompanyTool.php new file mode 100644 index 0000000000..9003ebf318 --- /dev/null +++ b/app/Mcp/Tools/ShowCompanyTool.php @@ -0,0 +1,89 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $company = null; + + if ($request->filled('id')) { + $company = Company::withCount([ + 'assets as assets_count' => fn ($q) => $q->AssetsForShow(), + ])->withCount('users as users_count')->find($request->get('id')); + } elseif ($request->filled('name')) { + $company = Company::withCount([ + 'assets as assets_count' => fn ($q) => $q->AssetsForShow(), + ])->withCount('users as users_count')->where('name', $request->get('name'))->first(); + } else { + return Response::make(Response::error('Please provide an id or name')); + } + + if (! $company) { + return Response::make(Response::error('Company not found')); + } + + if (! Gate::allows('view', $company)) { + return Response::make(Response::error('Unauthorized')); + } + + return Response::make( + Response::text('Company '.$company->name.' found') + )->withStructuredContent([ + 'id' => $company->id, + 'name' => $company->name, + 'phone' => $company->phone, + 'fax' => $company->fax, + 'email' => $company->email, + 'assets_count' => $company->assets_count, + 'users_count' => $company->users_count, + 'notes' => $company->notes, + 'created_at' => $company->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $company->updated_at?->format('Y-m-d H:i:s'), + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the company to look up'), + 'name' => $schema->string()->description('Name of the company to look up'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric company ID'), + 'name' => $schema->string()->description('Company name'), + 'phone' => $schema->string()->description('Company phone number'), + 'fax' => $schema->string()->description('Company fax number'), + 'email' => $schema->string()->description('Company email address'), + 'assets_count' => $schema->number()->description('Number of assets belonging to this company'), + 'users_count' => $schema->number()->description('Number of users belonging to this company'), + 'notes' => $schema->string()->description('Notes'), + 'created_at' => $schema->string()->description('Creation timestamp'), + 'updated_at' => $schema->string()->description('Last update timestamp'), + ]; + } +} diff --git a/app/Mcp/Tools/ShowConsumableTool.php b/app/Mcp/Tools/ShowConsumableTool.php new file mode 100644 index 0000000000..7436d12516 --- /dev/null +++ b/app/Mcp/Tools/ShowConsumableTool.php @@ -0,0 +1,107 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $consumable = null; + + if ($request->filled('id')) { + $consumable = Consumable::with('company', 'category', 'manufacturer', 'supplier', 'location')->find($request->get('id')); + } elseif ($request->filled('name')) { + $consumable = Consumable::with('company', 'category', 'manufacturer', 'supplier', 'location')->where('name', $request->get('name'))->first(); + } else { + return Response::make(Response::error('Either id or name is required')); + } + + if (! $consumable) { + return Response::make(Response::error('Consumable not found')); + } + + if (! Gate::allows('view', $consumable)) { + return Response::make(Response::error('Unauthorized')); + } + + $usersCount = $consumable->users()->count(); + + return Response::make( + Response::text('Consumable '.$consumable->name.' found') + )->withStructuredContent([ + 'id' => $consumable->id, + 'name' => $consumable->name, + 'qty' => $consumable->qty, + 'users_count' => $usersCount, + 'min_amt' => $consumable->min_amt, + 'category_id' => $consumable->category_id, + 'category' => $consumable->category?->name, + 'manufacturer_id' => $consumable->manufacturer_id, + 'manufacturer' => $consumable->manufacturer?->name, + 'company_id' => $consumable->company_id, + 'company' => $consumable->company?->name, + 'location_id' => $consumable->location_id, + 'location' => $consumable->location?->name, + 'purchase_cost' => $consumable->purchase_cost, + 'purchase_date' => $consumable->purchase_date?->format('Y-m-d'), + 'order_number' => $consumable->order_number, + 'model_number' => $consumable->model_number, + 'notes' => $consumable->notes, + 'created_at' => $consumable->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $consumable->updated_at?->format('Y-m-d H:i:s'), + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the consumable to look up'), + 'name' => $schema->string()->description('Name of the consumable to look up'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric consumable ID'), + 'name' => $schema->string()->description('Consumable name'), + 'qty' => $schema->number()->description('Total quantity in stock'), + 'users_count' => $schema->number()->description('Number of units checked out'), + 'min_amt' => $schema->number()->description('Minimum quantity alert threshold'), + 'category_id' => $schema->number()->description('Category ID'), + 'category' => $schema->string()->description('Category name'), + 'manufacturer_id' => $schema->number()->description('Manufacturer ID'), + 'manufacturer' => $schema->string()->description('Manufacturer name'), + 'company_id' => $schema->number()->description('Company ID'), + 'company' => $schema->string()->description('Company name'), + 'location_id' => $schema->number()->description('Location ID'), + 'location' => $schema->string()->description('Location name'), + 'purchase_cost' => $schema->string()->description('Purchase cost'), + 'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'), + 'order_number' => $schema->string()->description('Order number'), + 'model_number' => $schema->string()->description('Model number'), + 'notes' => $schema->string()->description('Notes'), + 'created_at' => $schema->string()->description('Creation timestamp'), + 'updated_at' => $schema->string()->description('Last updated timestamp'), + ]; + } +} diff --git a/app/Mcp/Tools/ShowDepreciationTool.php b/app/Mcp/Tools/ShowDepreciationTool.php new file mode 100644 index 0000000000..4af218731e --- /dev/null +++ b/app/Mcp/Tools/ShowDepreciationTool.php @@ -0,0 +1,87 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $depreciation = $this->resolveDepreciation($request); + + if ($depreciation === false) { + return Response::make(Response::error('Please provide an id or name')); + } + + if (! $depreciation) { + return Response::make(Response::error('Depreciation not found')); + } + + if (! Gate::allows('view', $depreciation)) { + return Response::make(Response::error('Unauthorized')); + } + + $depreciation->loadCount('models as models_count'); + + return Response::make( + Response::text('Depreciation '.$depreciation->name.' found') + )->withStructuredContent([ + 'id' => $depreciation->id, + 'name' => $depreciation->name, + 'months' => $depreciation->months, + 'models_count' => $depreciation->models_count, + 'created_at' => $depreciation->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $depreciation->updated_at?->format('Y-m-d H:i:s'), + ]); + } + + private function resolveDepreciation(Request $request): Depreciation|false|null + { + if ($request->filled('id')) { + return Depreciation::find($request->get('id')); + } + if ($request->filled('name')) { + return Depreciation::where('name', $request->get('name'))->first(); + } + + return false; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the depreciation to look up'), + 'name' => $schema->string()->description('Name of the depreciation to look up'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric depreciation ID'), + 'name' => $schema->string()->description('Depreciation name'), + 'months' => $schema->number()->description('Depreciation period in months'), + 'models_count' => $schema->number()->description('Number of asset models using this depreciation'), + 'created_at' => $schema->string()->description('Creation timestamp'), + 'updated_at' => $schema->string()->description('Last update timestamp'), + ]; + } +} diff --git a/app/Mcp/Tools/ShowGroupTool.php b/app/Mcp/Tools/ShowGroupTool.php new file mode 100644 index 0000000000..9cbe386579 --- /dev/null +++ b/app/Mcp/Tools/ShowGroupTool.php @@ -0,0 +1,75 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + if ($request->filled('id')) { + $group = Group::withCount('users as users_count')->find($request->get('id')); + } elseif ($request->filled('name')) { + $group = Group::withCount('users as users_count') + ->where('name', $request->get('name')) + ->first(); + } else { + return Response::make(Response::error('Please provide an id or name')); + } + + if (! $group) { + return Response::make(Response::error('Group not found')); + } + + return Response::make( + Response::text('Group '.$group->name.' found') + )->withStructuredContent([ + 'id' => $group->id, + 'name' => $group->name, + 'notes' => $group->notes, + 'permissions' => $group->decodePermissions(), + 'users_count' => $group->users_count, + ]); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric group ID'), + 'name' => $schema->string()->description('Group name to look up'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric group ID')->required(), + 'name' => $schema->string()->description('Group name')->required(), + 'notes' => $schema->string()->description('Notes about the group'), + 'permissions' => $schema->object()->description('Decoded permissions array'), + 'users_count' => $schema->number()->description('Number of users in this group'), + ]; + } +} diff --git a/app/Mcp/Tools/ShowLocationTool.php b/app/Mcp/Tools/ShowLocationTool.php new file mode 100644 index 0000000000..b3cf1ce746 --- /dev/null +++ b/app/Mcp/Tools/ShowLocationTool.php @@ -0,0 +1,109 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $location = $this->resolveLocation($request); + + if ($location === false) { + return Response::make(Response::error('Please provide an id or name')); + } + + if (! $location) { + return Response::make(Response::error('Location not found')); + } + + if (! Gate::allows('view', $location)) { + return Response::make(Response::error('Unauthorized')); + } + + $location->loadCount('assets as assets_count', 'users as users_count', 'children as children_count'); + + return Response::make( + Response::text('Location '.$location->name.' found') + )->withStructuredContent([ + 'id' => $location->id, + 'name' => $location->name, + 'address' => $location->address, + 'address2' => $location->address2, + 'city' => $location->city, + 'state' => $location->state, + 'country' => $location->country, + 'zip' => $location->zip, + 'phone' => $location->phone, + 'fax' => $location->fax, + 'parent_id' => $location->parent_id, + 'parent' => $location->parent?->name, + 'assets_count' => $location->assets_count, + 'users_count' => $location->users_count, + 'children_count' => $location->children_count, + 'created_at' => $location->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $location->updated_at?->format('Y-m-d H:i:s'), + ]); + } + + private function resolveLocation(Request $request): Location|false|null + { + if ($request->filled('id')) { + return Location::with('parent')->find($request->get('id')); + } + if ($request->filled('name')) { + return Location::with('parent')->where('name', $request->get('name'))->first(); + } + + return false; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the location to look up'), + 'name' => $schema->string()->description('Name of the location to look up'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric location ID'), + 'name' => $schema->string()->description('Location name'), + 'address' => $schema->string()->description('Street address'), + 'address2' => $schema->string()->description('Address line 2'), + 'city' => $schema->string()->description('City'), + 'state' => $schema->string()->description('State'), + 'country' => $schema->string()->description('Country'), + 'zip' => $schema->string()->description('Zip code'), + 'phone' => $schema->string()->description('Phone number'), + 'fax' => $schema->string()->description('Fax number'), + 'parent_id' => $schema->number()->description('Parent location ID'), + 'parent' => $schema->string()->description('Parent location name'), + 'assets_count' => $schema->number()->description('Number of assets at this location'), + 'users_count' => $schema->number()->description('Number of users at this location'), + 'children_count' => $schema->number()->description('Number of child locations'), + 'created_at' => $schema->string()->description('Creation timestamp'), + 'updated_at' => $schema->string()->description('Last update timestamp'), + ]; + } +} diff --git a/app/Mcp/Tools/ShowManufacturerTool.php b/app/Mcp/Tools/ShowManufacturerTool.php new file mode 100644 index 0000000000..671d06b630 --- /dev/null +++ b/app/Mcp/Tools/ShowManufacturerTool.php @@ -0,0 +1,97 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $manufacturer = $this->resolveManufacturer($request); + + if (! $manufacturer) { + if (! $request->filled('id') && ! $request->filled('name')) { + return Response::make(Response::error('Please provide an id or name')); + } + + return Response::make(Response::error('Manufacturer not found')); + } + + if (! Gate::allows('view', $manufacturer)) { + return Response::make(Response::error('Unauthorized')); + } + + $manufacturer->loadCount('assets as assets_count'); + + return Response::make( + Response::text('Manufacturer: '.$manufacturer->name) + )->withStructuredContent([ + 'id' => $manufacturer->id, + 'name' => $manufacturer->name, + 'url' => $manufacturer->url, + 'support_url' => $manufacturer->support_url, + 'support_email' => $manufacturer->support_email, + 'support_phone' => $manufacturer->support_phone, + 'warranty_lookup_url' => $manufacturer->warranty_lookup_url, + 'assets_count' => $manufacturer->assets_count, + 'notes' => $manufacturer->notes, + 'created_at' => $manufacturer->created_at?->toISOString(), + 'updated_at' => $manufacturer->updated_at?->toISOString(), + ]); + } + + private function resolveManufacturer(Request $request): ?Manufacturer + { + if ($request->filled('id')) { + return Manufacturer::find($request->get('id')); + } + if ($request->filled('name')) { + return Manufacturer::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the manufacturer to show'), + 'name' => $schema->string()->description('Name of the manufacturer to show'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the manufacturer'), + 'name' => $schema->string()->description('Manufacturer name')->required(), + 'url' => $schema->string()->description('Manufacturer website URL'), + 'support_url' => $schema->string()->description('Support website URL'), + 'support_email' => $schema->string()->description('Support email address'), + 'support_phone' => $schema->string()->description('Support phone number'), + 'warranty_lookup_url' => $schema->string()->description('Warranty lookup URL'), + 'assets_count' => $schema->number()->description('Number of assets from this manufacturer'), + 'notes' => $schema->string()->description('Notes'), + 'created_at' => $schema->string()->description('Creation timestamp'), + 'updated_at' => $schema->string()->description('Last update timestamp'), + ]; + } +} diff --git a/app/Mcp/Tools/ShowStatusLabelTool.php b/app/Mcp/Tools/ShowStatusLabelTool.php new file mode 100644 index 0000000000..08199ce7b3 --- /dev/null +++ b/app/Mcp/Tools/ShowStatusLabelTool.php @@ -0,0 +1,101 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $label = $this->resolveStatusLabel($request); + + if (! $label) { + if (! $request->filled('id') && ! $request->filled('name')) { + return Response::make(Response::error('Please provide an id or name')); + } + + return Response::make(Response::error('Status label not found')); + } + + if (! Gate::allows('view', $label)) { + return Response::make(Response::error('Unauthorized')); + } + + $label->loadCount('assets as assets_count'); + + return Response::make( + Response::text('Status label: '.$label->name) + )->withStructuredContent([ + 'id' => $label->id, + 'name' => $label->name, + 'type' => $label->getStatuslabelType(), + 'color' => $label->color, + 'deployable' => $label->deployable, + 'pending' => $label->pending, + 'archived' => $label->archived, + 'assets_count' => $label->assets_count, + 'default_label' => $label->default_label, + 'show_in_nav' => $label->show_in_nav, + 'notes' => $label->notes, + 'created_at' => $label->created_at?->toISOString(), + 'updated_at' => $label->updated_at?->toISOString(), + ]); + } + + private function resolveStatusLabel(Request $request): ?Statuslabel + { + if ($request->filled('id')) { + return Statuslabel::find($request->get('id')); + } + if ($request->filled('name')) { + return Statuslabel::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the status label to show'), + 'name' => $schema->string()->description('Name of the status label to show'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the status label'), + 'name' => $schema->string()->description('Status label name')->required(), + 'type' => $schema->string()->description('Status label type (deployable, pending, archived, undeployable)'), + 'color' => $schema->string()->description('Display color'), + 'deployable' => $schema->boolean()->description('Whether status is deployable'), + 'pending' => $schema->boolean()->description('Whether status is pending'), + 'archived' => $schema->boolean()->description('Whether status is archived'), + 'assets_count' => $schema->number()->description('Number of assets with this status'), + 'default_label' => $schema->boolean()->description('Whether this is the default label'), + 'show_in_nav' => $schema->boolean()->description('Whether to show in navigation'), + 'notes' => $schema->string()->description('Notes'), + 'created_at' => $schema->string()->description('Creation timestamp'), + 'updated_at' => $schema->string()->description('Last update timestamp'), + ]; + } +} diff --git a/app/Mcp/Tools/ShowSupplierTool.php b/app/Mcp/Tools/ShowSupplierTool.php new file mode 100644 index 0000000000..3ae06dedd9 --- /dev/null +++ b/app/Mcp/Tools/ShowSupplierTool.php @@ -0,0 +1,111 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + ]); + + $supplier = $this->resolveSupplier($request); + + if (! $supplier) { + if (! $request->filled('id') && ! $request->filled('name')) { + return Response::make(Response::error('Please provide an id or name')); + } + + return Response::make(Response::error('Supplier not found')); + } + + if (! Gate::allows('view', $supplier)) { + return Response::make(Response::error('Unauthorized')); + } + + $supplier->loadCount('assets as assets_count', 'licenses as licenses_count'); + + return Response::make( + Response::text('Supplier: '.$supplier->name) + )->withStructuredContent([ + 'id' => $supplier->id, + 'name' => $supplier->name, + 'address' => $supplier->address, + 'address2' => $supplier->address2, + 'city' => $supplier->city, + 'state' => $supplier->state, + 'country' => $supplier->country, + 'zip' => $supplier->zip, + 'phone' => $supplier->phone, + 'fax' => $supplier->fax, + 'email' => $supplier->email, + 'url' => $supplier->url, + 'contact' => $supplier->contact, + 'notes' => $supplier->notes, + 'assets_count' => $supplier->assets_count, + 'licenses_count' => $supplier->licenses_count, + 'created_at' => $supplier->created_at?->toISOString(), + 'updated_at' => $supplier->updated_at?->toISOString(), + ]); + } + + private function resolveSupplier(Request $request): ?Supplier + { + if ($request->filled('id')) { + return Supplier::find($request->get('id')); + } + if ($request->filled('name')) { + return Supplier::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the supplier to show'), + 'name' => $schema->string()->description('Name of the supplier to show'), + ]; + } + + public function outputSchema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID of the supplier'), + 'name' => $schema->string()->description('Supplier name')->required(), + 'address' => $schema->string()->description('Address line 1'), + 'address2' => $schema->string()->description('Address line 2'), + 'city' => $schema->string()->description('City'), + 'state' => $schema->string()->description('State'), + 'country' => $schema->string()->description('Country'), + 'zip' => $schema->string()->description('Postal code'), + 'phone' => $schema->string()->description('Phone number'), + 'fax' => $schema->string()->description('Fax number'), + 'email' => $schema->string()->description('Email address'), + 'url' => $schema->string()->description('Website URL'), + 'contact' => $schema->string()->description('Contact name'), + 'notes' => $schema->string()->description('Notes'), + 'assets_count' => $schema->number()->description('Number of assets from this supplier'), + 'licenses_count' => $schema->number()->description('Number of licenses from this supplier'), + 'created_at' => $schema->string()->description('Creation timestamp'), + 'updated_at' => $schema->string()->description('Last update timestamp'), + ]; + } +} diff --git a/app/Mcp/Tools/UpdateAssetModelTool.php b/app/Mcp/Tools/UpdateAssetModelTool.php new file mode 100644 index 0000000000..f40701def2 --- /dev/null +++ b/app/Mcp/Tools/UpdateAssetModelTool.php @@ -0,0 +1,111 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + 'new_name' => 'nullable|string|max:255', + 'category_id' => 'nullable|integer|exists:categories,id', + 'manufacturer_id' => 'nullable|integer|exists:manufacturers,id', + 'depreciation_id' => 'nullable|integer|exists:depreciations,id', + 'model_number' => 'nullable|string|max:255', + 'eol' => 'nullable|integer|min:0|max:240', + 'min_amt' => 'nullable|integer|min:0', + 'notes' => 'nullable|string', + 'requestable' => 'nullable|boolean', + 'require_serial' => 'nullable|boolean', + ]); + + $model = $this->resolveModel($request); + + if (! $model) { + return Response::make(Response::error('Asset model not found')); + } + + if (! Gate::allows('update', $model)) { + return Response::make(Response::error('Unauthorized')); + } + + if ($request->filled('new_name')) { + $model->name = $request->get('new_name'); + } + + foreach (['category_id', 'manufacturer_id', 'depreciation_id', 'model_number', 'eol', 'min_amt', 'notes', 'requestable', 'require_serial'] as $field) { + if ($request->filled($field)) { + $model->{$field} = $request->get($field); + } + } + + if ($model->save()) { + return Response::make( + Response::text('Asset model '.$model->name.' updated successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Asset model updated successfully', + 'id' => $model->id, + 'name' => $model->name, + ]); + } + + return Response::make(Response::error('Update failed: '.$model->getErrors()->first())); + } + + private function resolveModel(Request $request): ?AssetModel + { + if ($request->filled('id')) { + return AssetModel::find($request->get('id')); + } + if ($request->filled('name')) { + return AssetModel::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID to identify the asset model'), + 'name' => $schema->string()->description('Name to identify the asset model'), + 'new_name' => $schema->string()->description('New name (renames the asset model)'), + 'category_id' => $schema->number()->description('Category ID'), + 'manufacturer_id' => $schema->number()->description('Manufacturer ID'), + 'depreciation_id' => $schema->number()->description('Depreciation schedule ID'), + 'model_number' => $schema->string()->description('Model number'), + 'eol' => $schema->number()->description('End of life in months (0-240)'), + 'min_amt' => $schema->number()->description('Minimum quantity alert threshold'), + 'notes' => $schema->string()->description('Notes'), + 'requestable' => $schema->boolean()->description('Whether the model can be requested'), + 'require_serial' => $schema->boolean()->description('Whether serial numbers are required'), + ]; + } + + 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 asset model'), + 'name' => $schema->string()->description('Name of the asset model'), + ]; + } +} diff --git a/app/Mcp/Tools/UpdateCategoryTool.php b/app/Mcp/Tools/UpdateCategoryTool.php new file mode 100644 index 0000000000..048576393e --- /dev/null +++ b/app/Mcp/Tools/UpdateCategoryTool.php @@ -0,0 +1,105 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + 'new_name' => 'nullable|string|max:255', + 'category_type' => 'nullable|string|in:asset,accessory,consumable,component,license', + 'notes' => 'nullable|string', + 'checkin_email' => 'nullable|boolean', + 'require_acceptance' => 'nullable|boolean', + 'use_default_eula' => 'nullable|boolean', + ]); + + $category = $this->resolveCategory($request); + + if (! $category) { + return Response::make(Response::error('Category not found')); + } + + if (! Gate::allows('update', $category)) { + return Response::make(Response::error('Unauthorized')); + } + + if ($request->filled('new_name')) { + $category->name = $request->get('new_name'); + } + + foreach (['category_type', 'notes', 'checkin_email', 'require_acceptance', 'use_default_eula'] as $field) { + if ($request->filled($field)) { + $category->{$field} = $request->get($field); + } + } + + if ($category->save()) { + return Response::make( + Response::text('Category '.$category->name.' updated successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Category updated successfully', + 'id' => $category->id, + 'name' => $category->name, + 'category_type' => $category->category_type, + ]); + } + + return Response::make(Response::error('Update failed: '.$category->getErrors()->first())); + } + + private function resolveCategory(Request $request): ?Category + { + if ($request->filled('id')) { + return Category::find($request->get('id')); + } + if ($request->filled('name')) { + return Category::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID to identify the category'), + 'name' => $schema->string()->description('Name to identify the category'), + 'new_name' => $schema->string()->description('New name (renames the category)'), + 'category_type' => $schema->string()->description('Category type: asset, accessory, consumable, component, or license'), + 'checkin_email' => $schema->boolean()->description('Send checkin email when items are checked in'), + 'require_acceptance' => $schema->boolean()->description('Require user acceptance when checking out'), + 'use_default_eula' => $schema->boolean()->description('Use the default EULA'), + 'notes' => $schema->string()->description('Notes'), + ]; + } + + 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 category'), + 'name' => $schema->string()->description('Name of the category'), + 'category_type' => $schema->string()->description('Type of the category'), + ]; + } +} diff --git a/app/Mcp/Tools/UpdateCompanyTool.php b/app/Mcp/Tools/UpdateCompanyTool.php new file mode 100644 index 0000000000..0c7edec457 --- /dev/null +++ b/app/Mcp/Tools/UpdateCompanyTool.php @@ -0,0 +1,101 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + 'new_name' => 'nullable|string|max:255', + 'phone' => 'nullable|string', + 'fax' => 'nullable|string', + 'email' => 'nullable|string', + 'notes' => 'nullable|string', + ]); + + $company = $this->resolveCompany($request); + + if (! $company) { + return Response::make(Response::error('Company not found')); + } + + if (! Gate::allows('update', $company)) { + return Response::make(Response::error('Unauthorized')); + } + + if ($request->filled('new_name')) { + $company->name = $request->get('new_name'); + } + + foreach (['phone', 'fax', 'email', 'notes'] as $field) { + if ($request->filled($field)) { + $company->{$field} = $request->get($field); + } + } + + if ($company->save()) { + return Response::make( + Response::text('Company '.$company->name.' updated successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Company updated successfully', + 'id' => $company->id, + 'name' => $company->name, + ]); + } + + return Response::make(Response::error('Update failed: '.$company->getErrors()->first())); + } + + private function resolveCompany(Request $request): ?Company + { + if ($request->filled('id')) { + return Company::find($request->get('id')); + } + if ($request->filled('name')) { + return Company::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID to identify the company'), + 'name' => $schema->string()->description('Name to identify the company'), + 'new_name' => $schema->string()->description('New name (renames the company)'), + 'phone' => $schema->string()->description('Company phone number'), + 'fax' => $schema->string()->description('Company fax number'), + 'email' => $schema->string()->description('Company email address'), + 'notes' => $schema->string()->description('Notes'), + ]; + } + + 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 company'), + 'name' => $schema->string()->description('Name of the company'), + ]; + } +} diff --git a/app/Mcp/Tools/UpdateConsumableTool.php b/app/Mcp/Tools/UpdateConsumableTool.php new file mode 100644 index 0000000000..62f70beb2f --- /dev/null +++ b/app/Mcp/Tools/UpdateConsumableTool.php @@ -0,0 +1,120 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + 'new_name' => 'nullable|string|max:255', + 'qty' => 'nullable|integer|min:0', + 'category_id' => 'nullable|integer|exists:categories,id', + 'company_id' => 'nullable|integer', + 'location_id' => 'nullable|integer|exists:locations,id', + 'manufacturer_id' => 'nullable|integer|exists:manufacturers,id', + 'supplier_id' => 'nullable|integer', + 'purchase_cost' => 'nullable|numeric|min:0', + 'purchase_date' => 'nullable|date_format:Y-m-d', + 'min_amt' => 'nullable|integer|min:0', + 'notes' => 'nullable|string', + ]); + + $consumable = $this->resolveConsumable($request); + + if (! $consumable) { + return Response::make(Response::error('Consumable not found')); + } + + if (! Gate::allows('update', $consumable)) { + return Response::make(Response::error('Unauthorized')); + } + + $updatable = [ + 'qty', 'category_id', 'company_id', 'location_id', 'manufacturer_id', + 'supplier_id', 'purchase_cost', 'purchase_date', 'min_amt', 'notes', + ]; + + foreach ($updatable as $field) { + if ($request->filled($field)) { + $consumable->{$field} = $request->get($field); + } + } + + if ($request->filled('new_name')) { + $consumable->name = $request->get('new_name'); + } + + if ($consumable->save()) { + return Response::make( + Response::text('Consumable '.$consumable->name.' updated successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Consumable updated successfully', + 'id' => $consumable->id, + 'name' => $consumable->name, + 'qty' => $consumable->qty, + ]); + } + + return Response::make(Response::error('Update failed: '.$consumable->getErrors()->first())); + } + + private function resolveConsumable(Request $request): ?Consumable + { + if ($request->filled('id')) { + return Consumable::find($request->get('id')); + } + if ($request->filled('name')) { + return Consumable::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID to identify the consumable'), + 'name' => $schema->string()->description('Name to identify the consumable'), + 'new_name' => $schema->string()->description('New name (renames the consumable)'), + 'qty' => $schema->number()->description('Total quantity in stock'), + 'category_id' => $schema->number()->description('Category ID'), + 'company_id' => $schema->number()->description('Company ID'), + 'location_id' => $schema->number()->description('Location ID'), + 'manufacturer_id' => $schema->number()->description('Manufacturer ID'), + 'supplier_id' => $schema->number()->description('Supplier ID'), + 'purchase_cost' => $schema->number()->description('Purchase cost per unit'), + 'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'), + 'min_amt' => $schema->number()->description('Minimum quantity alert threshold'), + 'notes' => $schema->string()->description('Notes'), + ]; + } + + 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 consumable'), + 'name' => $schema->string()->description('Name of the consumable'), + 'qty' => $schema->number()->description('Total quantity'), + ]; + } +} diff --git a/app/Mcp/Tools/UpdateDepreciationTool.php b/app/Mcp/Tools/UpdateDepreciationTool.php new file mode 100644 index 0000000000..1fb2ba2223 --- /dev/null +++ b/app/Mcp/Tools/UpdateDepreciationTool.php @@ -0,0 +1,95 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + 'new_name' => 'nullable|string|max:255', + 'months' => 'nullable|integer|min:1|max:3600', + ]); + + $dep = $this->resolveDepreciation($request); + + if (! $dep) { + return Response::make(Response::error('Depreciation not found')); + } + + if (! Gate::allows('update', $dep)) { + return Response::make(Response::error('Unauthorized')); + } + + if ($request->filled('new_name')) { + $dep->name = $request->get('new_name'); + } + + if ($request->filled('months')) { + $dep->months = $request->get('months'); + } + + if ($dep->save()) { + return Response::make( + Response::text('Depreciation '.$dep->name.' updated successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Depreciation updated successfully', + 'id' => $dep->id, + 'name' => $dep->name, + 'months' => $dep->months, + ]); + } + + return Response::make(Response::error('Update failed: '.$dep->getErrors()->first())); + } + + private function resolveDepreciation(Request $request): ?Depreciation + { + if ($request->filled('id')) { + return Depreciation::find($request->get('id')); + } + if ($request->filled('name')) { + return Depreciation::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID to identify the depreciation'), + 'name' => $schema->string()->description('Name to identify the depreciation'), + 'new_name' => $schema->string()->description('New name (renames the depreciation)'), + 'months' => $schema->number()->description('Depreciation period in months (1-3600)'), + ]; + } + + 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 depreciation'), + 'name' => $schema->string()->description('Name of the depreciation'), + 'months' => $schema->number()->description('Depreciation period in months'), + ]; + } +} diff --git a/app/Mcp/Tools/UpdateGroupTool.php b/app/Mcp/Tools/UpdateGroupTool.php new file mode 100644 index 0000000000..286c367af2 --- /dev/null +++ b/app/Mcp/Tools/UpdateGroupTool.php @@ -0,0 +1,92 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + 'new_name' => 'nullable|string|max:255', + 'notes' => 'nullable|string', + ]); + } catch (ValidationException $e) { + return Response::make(Response::error($e->validator->errors()->first())); + } + + if ($request->filled('id')) { + $group = Group::find($request->get('id')); + } elseif ($request->filled('name')) { + $group = Group::where('name', $request->get('name'))->first(); + } else { + return Response::make(Response::error('Please provide an id or name')); + } + + if (! $group) { + return Response::make(Response::error('Group not found')); + } + + if ($request->filled('new_name')) { + $group->name = $request->get('new_name'); + } + + if ($request->filled('notes')) { + $group->notes = $request->get('notes'); + } + + if ($group->save()) { + return Response::make( + Response::text('Group '.$group->name.' updated successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Group updated successfully', + 'id' => $group->id, + 'name' => $group->name, + ]); + } + + return Response::make(Response::error('Update failed: '.$group->getErrors()->first())); + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric group ID to update'), + 'name' => $schema->string()->description('Group name to look up for updating'), + 'new_name' => $schema->string()->description('New name to rename the group to'), + 'notes' => $schema->string()->description('Updated notes for the group'), + ]; + } + + 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 updated group'), + 'name' => $schema->string()->description('Name of the updated group'), + ]; + } +} diff --git a/app/Mcp/Tools/UpdateLocationTool.php b/app/Mcp/Tools/UpdateLocationTool.php new file mode 100644 index 0000000000..27af3a1468 --- /dev/null +++ b/app/Mcp/Tools/UpdateLocationTool.php @@ -0,0 +1,113 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + 'new_name' => 'nullable|string|max:255', + 'address' => 'nullable|string', + 'city' => 'nullable|string', + 'state' => 'nullable|string', + 'country' => 'nullable|string', + 'zip' => 'nullable|string', + 'phone' => 'nullable|string', + 'fax' => 'nullable|string', + 'currency' => 'nullable|string', + 'parent_id' => 'nullable|integer|exists:locations,id', + 'manager_id' => 'nullable|integer|exists:users,id', + ]); + + $location = $this->resolveLocation($request); + + if (! $location) { + return Response::make(Response::error('Location not found')); + } + + if (! Gate::allows('update', $location)) { + return Response::make(Response::error('Unauthorized')); + } + + if ($request->filled('new_name')) { + $location->name = $request->get('new_name'); + } + + foreach (['address', 'city', 'state', 'country', 'zip', 'phone', 'fax', 'currency', 'parent_id', 'manager_id'] as $field) { + if ($request->filled($field)) { + $location->{$field} = $request->get($field); + } + } + + if ($location->save()) { + return Response::make( + Response::text('Location '.$location->name.' updated successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Location updated successfully', + 'id' => $location->id, + 'name' => $location->name, + ]); + } + + return Response::make(Response::error('Update failed: '.$location->getErrors()->first())); + } + + private function resolveLocation(Request $request): ?Location + { + if ($request->filled('id')) { + return Location::find($request->get('id')); + } + if ($request->filled('name')) { + return Location::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID to identify the location'), + 'name' => $schema->string()->description('Name to identify the location'), + 'new_name' => $schema->string()->description('New name (renames the location)'), + 'address' => $schema->string()->description('Street address'), + 'city' => $schema->string()->description('City'), + 'state' => $schema->string()->description('State'), + 'country' => $schema->string()->description('Country'), + 'zip' => $schema->string()->description('Zip code'), + 'phone' => $schema->string()->description('Phone number'), + 'fax' => $schema->string()->description('Fax number'), + 'currency' => $schema->string()->description('Currency code'), + 'parent_id' => $schema->number()->description('Parent location ID'), + 'manager_id' => $schema->number()->description('Manager user ID'), + ]; + } + + 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 location'), + 'name' => $schema->string()->description('Name of the location'), + ]; + } +} diff --git a/app/Mcp/Tools/UpdateManufacturerTool.php b/app/Mcp/Tools/UpdateManufacturerTool.php new file mode 100644 index 0000000000..11a79272bb --- /dev/null +++ b/app/Mcp/Tools/UpdateManufacturerTool.php @@ -0,0 +1,107 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + 'new_name' => 'nullable|string|max:255', + 'url' => 'nullable|string|max:255', + 'support_url' => 'nullable|string|max:255', + 'support_email' => 'nullable|email|max:191', + 'support_phone' => 'nullable|string|max:191', + 'warranty_lookup_url' => 'nullable|string|max:255', + 'notes' => 'nullable|string', + ]); + + $manufacturer = $this->resolveManufacturer($request); + + if (! $manufacturer) { + return Response::make(Response::error('Manufacturer not found')); + } + + if (! Gate::allows('update', $manufacturer)) { + return Response::make(Response::error('Unauthorized')); + } + + if ($request->filled('new_name')) { + $manufacturer->name = $request->get('new_name'); + } + + $updatable = ['url', 'support_url', 'support_email', 'support_phone', 'warranty_lookup_url', 'notes']; + + foreach ($updatable as $field) { + if ($request->filled($field)) { + $manufacturer->{$field} = $request->get($field); + } + } + + if ($manufacturer->save()) { + return Response::make( + Response::text('Manufacturer '.$manufacturer->name.' updated successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Manufacturer updated successfully', + 'id' => $manufacturer->id, + 'name' => $manufacturer->name, + ]); + } + + return Response::make(Response::error('Update failed: '.$manufacturer->getErrors()->first())); + } + + private function resolveManufacturer(Request $request): ?Manufacturer + { + if ($request->filled('id')) { + return Manufacturer::find($request->get('id')); + } + if ($request->filled('name')) { + return Manufacturer::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID to identify the manufacturer'), + 'name' => $schema->string()->description('Name to identify the manufacturer'), + 'new_name' => $schema->string()->description('New name (renames the manufacturer)'), + 'url' => $schema->string()->description('Manufacturer website URL'), + 'support_url' => $schema->string()->description('Support website URL'), + 'support_email' => $schema->string()->description('Support email address'), + 'support_phone' => $schema->string()->description('Support phone number'), + 'warranty_lookup_url' => $schema->string()->description('Warranty lookup URL'), + 'notes' => $schema->string()->description('Notes'), + ]; + } + + 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 manufacturer'), + 'name' => $schema->string()->description('Name of the manufacturer'), + ]; + } +} diff --git a/app/Mcp/Tools/UpdateStatusLabelTool.php b/app/Mcp/Tools/UpdateStatusLabelTool.php new file mode 100644 index 0000000000..66676a4861 --- /dev/null +++ b/app/Mcp/Tools/UpdateStatusLabelTool.php @@ -0,0 +1,122 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + 'new_name' => 'nullable|string|max:255', + 'type' => 'nullable|string|in:deployable,pending,archived,undeployable', + 'color' => 'nullable|string', + 'notes' => 'nullable|string', + 'default_label' => 'nullable|boolean', + 'show_in_nav' => 'nullable|boolean', + ]); + + $label = $this->resolveStatusLabel($request); + + if (! $label) { + return Response::make(Response::error('Status label not found')); + } + + if (! Gate::allows('update', $label)) { + return Response::make(Response::error('Unauthorized')); + } + + if ($request->filled('new_name')) { + $label->name = $request->get('new_name'); + } + + if ($request->filled('type')) { + $statusType = Statuslabel::getStatuslabelTypesForDB($request->get('type')); + $label->deployable = $statusType['deployable']; + $label->pending = $statusType['pending']; + $label->archived = $statusType['archived']; + } + + if ($request->filled('color')) { + $label->color = $request->get('color'); + } + + if ($request->filled('notes')) { + $label->notes = $request->get('notes'); + } + + if ($request->has('default_label')) { + $label->default_label = $request->get('default_label'); + } + + if ($request->has('show_in_nav')) { + $label->show_in_nav = $request->get('show_in_nav'); + } + + if ($label->save()) { + return Response::make( + Response::text('Status label '.$label->name.' updated successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Status label updated successfully', + 'id' => $label->id, + 'name' => $label->name, + 'type' => $label->getStatuslabelType(), + ]); + } + + return Response::make(Response::error('Update failed: '.$label->getErrors()->first())); + } + + private function resolveStatusLabel(Request $request): ?Statuslabel + { + if ($request->filled('id')) { + return Statuslabel::find($request->get('id')); + } + if ($request->filled('name')) { + return Statuslabel::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID to identify the status label'), + 'name' => $schema->string()->description('Name to identify the status label'), + 'new_name' => $schema->string()->description('New name (renames the status label)'), + 'type' => $schema->string()->description('New type: deployable, pending, archived, or undeployable'), + 'color' => $schema->string()->description('Display color in #RRGGBB format'), + 'notes' => $schema->string()->description('Notes'), + 'default_label' => $schema->boolean()->description('Whether this is the default label'), + 'show_in_nav' => $schema->boolean()->description('Whether to show in navigation'), + ]; + } + + 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 status label'), + 'name' => $schema->string()->description('Name of the status label'), + 'type' => $schema->string()->description('Type of the status label'), + ]; + } +} diff --git a/app/Mcp/Tools/UpdateSupplierTool.php b/app/Mcp/Tools/UpdateSupplierTool.php new file mode 100644 index 0000000000..ccfe22b214 --- /dev/null +++ b/app/Mcp/Tools/UpdateSupplierTool.php @@ -0,0 +1,119 @@ +validate([ + 'id' => 'nullable|integer', + 'name' => 'nullable|string|max:255', + 'new_name' => 'nullable|string|max:255', + 'address' => 'nullable|string', + 'address2' => 'nullable|string', + 'city' => 'nullable|string', + 'state' => 'nullable|string', + 'country' => 'nullable|string', + 'zip' => 'nullable|string', + 'phone' => 'nullable|string', + 'fax' => 'nullable|string', + 'email' => 'nullable|email', + 'url' => 'nullable|string', + 'contact' => 'nullable|string', + 'notes' => 'nullable|string', + ]); + + $supplier = $this->resolveSupplier($request); + + if (! $supplier) { + return Response::make(Response::error('Supplier not found')); + } + + if (! Gate::allows('update', $supplier)) { + return Response::make(Response::error('Unauthorized')); + } + + if ($request->filled('new_name')) { + $supplier->name = $request->get('new_name'); + } + + $updatable = ['address', 'address2', 'city', 'state', 'country', 'zip', 'phone', 'fax', 'email', 'url', 'contact', 'notes']; + + foreach ($updatable as $field) { + if ($request->filled($field)) { + $supplier->{$field} = $request->get($field); + } + } + + if ($supplier->save()) { + return Response::make( + Response::text('Supplier '.$supplier->name.' updated successfully') + )->withStructuredContent([ + 'success' => true, + 'message' => 'Supplier updated successfully', + 'id' => $supplier->id, + 'name' => $supplier->name, + ]); + } + + return Response::make(Response::error('Update failed: '.$supplier->getErrors()->first())); + } + + private function resolveSupplier(Request $request): ?Supplier + { + if ($request->filled('id')) { + return Supplier::find($request->get('id')); + } + if ($request->filled('name')) { + return Supplier::where('name', $request->get('name'))->first(); + } + + return null; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'id' => $schema->number()->description('Numeric ID to identify the supplier'), + 'name' => $schema->string()->description('Name to identify the supplier'), + 'new_name' => $schema->string()->description('New name (renames the supplier)'), + 'address' => $schema->string()->description('Address line 1'), + 'address2' => $schema->string()->description('Address line 2'), + 'city' => $schema->string()->description('City'), + 'state' => $schema->string()->description('State'), + 'country' => $schema->string()->description('Country'), + 'zip' => $schema->string()->description('Postal code'), + 'phone' => $schema->string()->description('Phone number'), + 'fax' => $schema->string()->description('Fax number'), + 'email' => $schema->string()->description('Email address'), + 'url' => $schema->string()->description('Website URL'), + 'contact' => $schema->string()->description('Contact name'), + 'notes' => $schema->string()->description('Notes'), + ]; + } + + 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 supplier'), + 'name' => $schema->string()->description('Name of the supplier'), + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index c7630f6338..a6ba7e40b0 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -180,16 +180,26 @@ class UserFactory extends Factory return $this->appendPermission(['assets.view.requestable' => '1']); } - public function deleteAssetModels() - { - return $this->appendPermission(['models.delete' => '1']); - } - public function viewAssetModels() { return $this->appendPermission(['models.view' => '1']); } + public function createAssetModels() + { + return $this->appendPermission(['models.create' => '1']); + } + + public function editAssetModels() + { + return $this->appendPermission(['models.edit' => '1']); + } + + public function deleteAssetModels() + { + return $this->appendPermission(['models.delete' => '1']); + } + public function viewAccessories() { return $this->appendPermission(['accessories.view' => '1']); @@ -370,11 +380,41 @@ class UserFactory extends Factory return $this->appendPermission(['users.delete' => '1']); } + public function viewCategories() + { + return $this->appendPermission(['categories.view' => '1']); + } + + public function createCategories() + { + return $this->appendPermission(['categories.create' => '1']); + } + + public function editCategories() + { + return $this->appendPermission(['categories.edit' => '1']); + } + public function deleteCategories() { return $this->appendPermission(['categories.delete' => '1']); } + public function viewLocations() + { + return $this->appendPermission(['locations.view' => '1']); + } + + public function createLocations() + { + return $this->appendPermission(['locations.create' => '1']); + } + + public function editLocations() + { + return $this->appendPermission(['locations.edit' => '1']); + } + public function deleteLocations() { return $this->appendPermission(['locations.delete' => '1']); @@ -415,11 +455,41 @@ class UserFactory extends Factory return $this->appendPermission(['customfields.delete' => '1']); } + public function viewDepreciations() + { + return $this->appendPermission(['depreciations.view' => '1']); + } + + public function createDepreciations() + { + return $this->appendPermission(['depreciations.create' => '1']); + } + + public function editDepreciations() + { + return $this->appendPermission(['depreciations.edit' => '1']); + } + public function deleteDepreciations() { return $this->appendPermission(['depreciations.delete' => '1']); } + public function viewManufacturers() + { + return $this->appendPermission(['manufacturers.view' => '1']); + } + + public function createManufacturers() + { + return $this->appendPermission(['manufacturers.create' => '1']); + } + + public function editManufacturers() + { + return $this->appendPermission(['manufacturers.edit' => '1']); + } + public function deleteManufacturers() { return $this->appendPermission(['manufacturers.delete' => '1']); @@ -435,11 +505,41 @@ class UserFactory extends Factory return $this->appendPermission(['kits.view' => '1']); } + public function viewStatusLabels() + { + return $this->appendPermission(['statuslabels.view' => '1']); + } + + public function createStatusLabels() + { + return $this->appendPermission(['statuslabels.create' => '1']); + } + + public function editStatusLabels() + { + return $this->appendPermission(['statuslabels.edit' => '1']); + } + public function deleteStatusLabels() { return $this->appendPermission(['statuslabels.delete' => '1']); } + public function viewSuppliers() + { + return $this->appendPermission(['suppliers.view' => '1']); + } + + public function createSuppliers() + { + return $this->appendPermission(['suppliers.create' => '1']); + } + + public function editSuppliers() + { + return $this->appendPermission(['suppliers.edit' => '1']); + } + public function deleteSuppliers() { return $this->appendPermission(['suppliers.delete' => '1']); diff --git a/tests/Feature/Mcp/CreateAssetModelToolTest.php b/tests/Feature/Mcp/CreateAssetModelToolTest.php index ae5ee162b0..327dc7b988 100644 --- a/tests/Feature/Mcp/CreateAssetModelToolTest.php +++ b/tests/Feature/Mcp/CreateAssetModelToolTest.php @@ -3,7 +3,6 @@ namespace Tests\Feature\Mcp; use App\Mcp\Tools\CreateAssetModelTool; -use App\Models\AssetModel; use App\Models\Category; use App\Models\User; use Laravel\Mcp\Request; diff --git a/tests/Feature/Mcp/CreateAssetToolTest.php b/tests/Feature/Mcp/CreateAssetToolTest.php index 9890282c2a..bd59641c79 100644 --- a/tests/Feature/Mcp/CreateAssetToolTest.php +++ b/tests/Feature/Mcp/CreateAssetToolTest.php @@ -3,7 +3,6 @@ namespace Tests\Feature\Mcp; use App\Mcp\Tools\CreateAssetTool; -use App\Models\Asset; use App\Models\AssetModel; use App\Models\Statuslabel; use App\Models\User; diff --git a/tests/Feature/Mcp/CreateDepreciationToolTest.php b/tests/Feature/Mcp/CreateDepreciationToolTest.php index 456b15948a..34a0f9049b 100644 --- a/tests/Feature/Mcp/CreateDepreciationToolTest.php +++ b/tests/Feature/Mcp/CreateDepreciationToolTest.php @@ -3,7 +3,6 @@ namespace Tests\Feature\Mcp; use App\Mcp\Tools\CreateDepreciationTool; -use App\Models\Depreciation; use App\Models\User; use Laravel\Mcp\Request; use Laravel\Mcp\ResponseFactory; diff --git a/tests/Feature/Mcp/CreateGroupToolTest.php b/tests/Feature/Mcp/CreateGroupToolTest.php index e5b8258f96..67fcd2b7ca 100644 --- a/tests/Feature/Mcp/CreateGroupToolTest.php +++ b/tests/Feature/Mcp/CreateGroupToolTest.php @@ -23,7 +23,7 @@ class CreateGroupToolTest extends TestCase public function test_creates_group() { - $name = 'Test MCP Group ' . uniqid(); + $name = 'Test MCP Group '.uniqid(); $this->handle(['name' => $name]); @@ -32,7 +32,7 @@ class CreateGroupToolTest extends TestCase public function test_response_includes_id_and_name() { - $name = 'Test MCP Group ' . uniqid(); + $name = 'Test MCP Group '.uniqid(); $content = $this->handle(['name' => $name])->getStructuredContent(); @@ -51,7 +51,7 @@ class CreateGroupToolTest extends TestCase { $this->actingAs(User::factory()->create()); - $name = 'Unauthorized Group ' . uniqid(); + $name = 'Unauthorized Group '.uniqid(); $this->handle(['name' => $name]); diff --git a/tests/Feature/Mcp/UpdateGroupToolTest.php b/tests/Feature/Mcp/UpdateGroupToolTest.php index cce215a058..78d6950e21 100644 --- a/tests/Feature/Mcp/UpdateGroupToolTest.php +++ b/tests/Feature/Mcp/UpdateGroupToolTest.php @@ -25,7 +25,7 @@ class UpdateGroupToolTest extends TestCase public function test_updates_group_by_id() { $group = Group::factory()->create(); - $newNotes = 'Updated notes ' . uniqid(); + $newNotes = 'Updated notes '.uniqid(); $this->handle(['id' => $group->id, 'notes' => $newNotes]); @@ -35,7 +35,7 @@ class UpdateGroupToolTest extends TestCase public function test_renames_via_new_name() { $group = Group::factory()->create(); - $newName = 'Renamed Group ' . uniqid(); + $newName = 'Renamed Group '.uniqid(); $this->handle(['id' => $group->id, 'new_name' => $newName]);