Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f697ef1d03 | |||
| 256003b675 | |||
| 464db7f473 | |||
| a56426e6f4 | |||
| 19e58a8640 | |||
| d83b64ff32 | |||
| e839d989ec | |||
| b8d2be6c3a | |||
| b264e07327 | |||
| 25a08faa6d | |||
| 926afa6c28 | |||
| e3a042f334 | |||
| 082ebeb27f | |||
| aed11dfce7 | |||
| 4090e05536 | |||
| 49818175cd | |||
| ef4b2349eb | |||
| 926f7dd5f7 | |||
| 8ccc705473 | |||
| c75d0effe2 | |||
| 96a3a11f00 | |||
| 9c97a06c7e | |||
| 2542221fc9 | |||
| 664a1906c1 | |||
| 08b2d0c85d | |||
| dc9f0104f6 | |||
| 6b2f2d68b7 | |||
| 9aa5ba5cd0 | |||
| b74e79b814 | |||
| 7636c2436c | |||
| 0eec6e3688 | |||
| d961714358 | |||
| 51bdc3b020 | |||
| 6a47b4e6a7 | |||
| 656dae04a7 | |||
| 2f3df9a085 | |||
| 0514901cbc | |||
| cc0169d2f7 | |||
| 490ce6fa5d | |||
| b731ec6dd6 | |||
| 91bd2064fd | |||
| deb56f250f | |||
| 7d57ce4679 | |||
| 84fea96949 | |||
| eada5f503c |
@@ -1,6 +1,7 @@
|
||||
# GitHub Copilot Custom Instructions for Snipe-IT
|
||||
|
||||
These instructions guide Copilot to generate code that aligns with modern Laravel 11 standards, PHP 8.2/8.4 features, software engineering principles, and industry best practices to improve software quality, maintainability, and security.
|
||||
These instructions guide Copilot to generate code that aligns with modern Laravel 12 standards, PHP 8.2/8.4 features,
|
||||
software engineering principles, and industry best practices to improve software quality, maintainability, and security.
|
||||
|
||||
## ✅ General Coding Standards
|
||||
|
||||
@@ -22,7 +23,7 @@ These instructions guide Copilot to generate code that aligns with modern Larave
|
||||
- Adopt **final classes** where extension is not intended.
|
||||
- Use **Named Arguments** for improved clarity when calling functions with multiple parameters.
|
||||
|
||||
## ✅ Laravel 11 Project Structure & Conventions
|
||||
## ✅ Laravel 12 Project Structure & Conventions
|
||||
|
||||
- Follow the official Laravel project structure:
|
||||
- `app/Http/Controllers` - Controllers
|
||||
@@ -32,6 +33,7 @@ These instructions guide Copilot to generate code that aligns with modern Larave
|
||||
- `app/Enums` - Enums
|
||||
- `app/Actions` - Single-responsibility action classes
|
||||
- `app/Policies` - Authorization logic
|
||||
- `app/Models/Builders` - Query scoping logic
|
||||
|
||||
- Controllers must:
|
||||
- Use dependency injection.
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# EthicalCheck addresses the critical need to continuously security test APIs in development and in production.
|
||||
|
||||
# EthicalCheck provides the industry’s only free & automated API security testing service that uncovers security vulnerabilities using OWASP API list.
|
||||
# Developers relies on EthicalCheck to evaluate every update and release, ensuring that no APIs go to production with exploitable vulnerabilities.
|
||||
|
||||
# You develop the application and API, we bring complete and continuous security testing to you, accelerating development.
|
||||
|
||||
# Know your API and Applications are secure with EthicalCheck – our free & automated API security testing service.
|
||||
|
||||
# How EthicalCheck works?
|
||||
# EthicalCheck functions in the following simple steps.
|
||||
# 1. Security Testing.
|
||||
# Provide your OpenAPI specification or start with a public Postman collection URL.
|
||||
# EthicalCheck instantly instrospects your API and creates a map of API endpoints for security testing.
|
||||
# It then automatically creates hundreds of security tests that are non-intrusive to comprehensively and completely test for authentication, authorizations, and OWASP bugs your API. The tests addresses the OWASP API Security categories including OAuth 2.0, JWT, Rate Limit etc.
|
||||
|
||||
# 2. Reporting.
|
||||
# EthicalCheck generates security test report that includes all the tested endpoints, coverage graph, exceptions, and vulnerabilities.
|
||||
# Vulnerabilities are fully triaged, it contains CVSS score, severity, endpoint information, and OWASP tagging.
|
||||
|
||||
|
||||
# This is a starter workflow to help you get started with EthicalCheck Actions
|
||||
|
||||
name: EthicalCheck-Workflow
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the "master" branch
|
||||
# Customize trigger events based on your DevSecOps processes.
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '35 17 * * 6'
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
Trigger_EthicalCheck:
|
||||
permissions:
|
||||
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
|
||||
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: EthicalCheck Free & Automated API Security Testing Service
|
||||
uses: apisec-inc/ethicalcheck-action@005fac321dd843682b1af6b72f30caaf9952c641
|
||||
with:
|
||||
# The OpenAPI Specification URL or Swagger Path or Public Postman collection URL.
|
||||
oas-url: "http://netbanking.apisec.ai:8080/v2/api-docs"
|
||||
# The email address to which the penetration test report will be sent.
|
||||
email: "snipe@snipe.net"
|
||||
sarif-result-file: "ethicalcheck-results.sarif"
|
||||
|
||||
- name: Upload sarif file to repository
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: ./ethicalcheck-results.sarif
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Stack
|
||||
|
||||
- **PHP 8.2+** / **Laravel 12** (framework), **Laravel Mix** (webpack) for frontend assets
|
||||
- **AdminLTE 2** / **Bootstrap 3** UI — Blade views, no Livewire/Inertia
|
||||
- **Chart.js v2.9.4** — bundled at `public/js/dist/Chart.min.js`; use `horizontalBar` type (v2 API, not v3)
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
php artisan test
|
||||
# or
|
||||
vendor/bin/phpunit
|
||||
|
||||
# Run a single test file
|
||||
php artisan test tests/Feature/Assets/AssetsTest.php
|
||||
|
||||
# Run a specific test method
|
||||
php artisan test --filter testSomeMethod
|
||||
|
||||
# Build frontend assets (dev)
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run prod
|
||||
|
||||
# Laravel Mix watch
|
||||
npm run watch
|
||||
|
||||
# Tinker / REPL
|
||||
php artisan tinker
|
||||
|
||||
# Clear caches after config/route changes
|
||||
php artisan optimize:clear
|
||||
```
|
||||
|
||||
Dev server is served via **Laravel Herd** (`herd coverage` for coverage reports).
|
||||
|
||||
## Architecture
|
||||
|
||||
### Controllers
|
||||
|
||||
Two parallel controller trees:
|
||||
- `app/Http/Controllers/` — web/UI controllers (Blade views)
|
||||
- `app/Http/Controllers/Api/` — REST API controllers (JSON, used by datatables + select2)
|
||||
|
||||
Subdirectory groupings: `Assets/`, `Licenses/`, `Users/`, `Accessories/`, `Consumables/`, `Components/`, `Kits/`, `Account/`, `Auth/`
|
||||
|
||||
### API Pattern
|
||||
|
||||
Every API controller returns data via a **Transformer** (`app/Http/Transformers/`). Never return raw model attributes from API controllers — always pass through the transformer. `DatatablesTransformer` wraps paginated results.
|
||||
|
||||
```php
|
||||
return (new AssetsTransformer)->transformAssets($assets, $assets->count());
|
||||
```
|
||||
|
||||
### Authorization
|
||||
|
||||
All authorization goes through **Policies** (`app/Policies/`). `CheckoutablePermissionsPolicy` is the base for assets/licenses/accessories/consumables — its `checkout()` / `checkin()` methods accept `$item = null` so you can use `@can('checkout', \App\Models\Asset::class)` without an instance.
|
||||
|
||||
### FMCS (Full Multiple Company Support)
|
||||
|
||||
`Setting::getSettings()->full_multiple_companies_support == '1'` gates company-scoped filtering. The select2 API endpoints (`selectlist()` methods) accept a `companyId` query param — apply it like this:
|
||||
|
||||
```php
|
||||
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($request->filled('companyId'))) {
|
||||
$query->where('table.company_id', $request->input('companyId'));
|
||||
}
|
||||
```
|
||||
|
||||
Pass `data-company-id="{{ $user->company_id }}"` in Blade to wire it to select2.
|
||||
|
||||
### Select2 AJAX Dropdowns
|
||||
|
||||
Use `class="js-data-ajax"` with `data-endpoint="hardware|licenses|consumables|..."`. `snipeit.js` auto-initializes these, forwarding `data-company-id` as `companyId` and `data-asset-status-type` as `statusType` to the API.
|
||||
|
||||
### Routes
|
||||
|
||||
All routes are in `routes/web.php` (UI) and `routes/api.php` (API). Breadcrumbs are defined inline using `->breadcrumbs(fn (Trail $trail) => ...)` from `tabuna/breadcrumbs`. Every UI route should have a breadcrumb.
|
||||
|
||||
Note: the `reports/unaccepted_assets` route is named with slashes, not dots — use `route('reports/unaccepted_assets')`.
|
||||
|
||||
### Translations
|
||||
|
||||
String keys live in `resources/lang/en-US/general.php` (and other files in that directory). Always add new UI strings as translation keys rather than hard-coding English.
|
||||
|
||||
### Checkout Redirect Flow
|
||||
|
||||
After checkout, `Helper::getRedirectOption()` reads `$request->redirect_option`. For redirecting back to the assigned user after checkout:
|
||||
- Set `redirect_option=target` in the form
|
||||
- Set `checkout_to_type=user` in the form
|
||||
- Set `assigned_user={{ $user->id }}` in the form
|
||||
|
||||
### Key Helper Methods (`app/Helpers/Helper.php`)
|
||||
|
||||
- `Helper::deployableStatusLabelList()` — status labels for checkout forms
|
||||
- `Helper::defaultChartColors()` — 10-color palette used in charts
|
||||
- `Helper::getRedirectOption($request, $id, $table)` — post-checkout redirect logic
|
||||
|
||||
### Global View Variables
|
||||
|
||||
`$snipeSettings` is injected into all views via a service provider — no need to pass `Setting::getSettings()` from every controller. Use it directly in Blade.
|
||||
|
||||
## Testing
|
||||
|
||||
Tests live in `tests/Feature/` (organized by entity) and `tests/Unit/`. Feature tests hit the database; the test environment uses `array` cache/session/mail drivers. Tests use factories for data setup.
|
||||
@@ -15,13 +15,11 @@ use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\AccessoryCheckout;
|
||||
use App\Models\Company;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AccessoriesController extends Controller
|
||||
{
|
||||
@@ -302,49 +300,40 @@ class AccessoriesController extends Controller
|
||||
{
|
||||
$this->authorize('checkout', $accessory);
|
||||
$target = $this->determineCheckoutTarget();
|
||||
$accessory->checkout_qty = $request->input('checkout_qty', 1);
|
||||
|
||||
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($accessory->company_id !== $target->company_id)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
|
||||
|
||||
$accessory_checkout = new AccessoryCheckout([
|
||||
'accessory_id' => $accessory->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_to' => $target->id,
|
||||
'assigned_type' => $target::class,
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
|
||||
$accessory_checkout->created_by = auth()->id();
|
||||
$accessory_checkout->save();
|
||||
|
||||
$payload = [
|
||||
'accessory_id' => $accessory->id,
|
||||
'assigned_to' => $target->id,
|
||||
'assigned_type' => $target::class,
|
||||
'note' => $request->input('note'),
|
||||
'created_by' => auth()->id(),
|
||||
'pivot' => $accessory_checkout->id,
|
||||
];
|
||||
}
|
||||
|
||||
$accessory->checkout_qty = $request->input('checkout_qty', 1);
|
||||
$payload = null;
|
||||
|
||||
// Keep checkout rows and checkout log/event atomic to avoid ghost assignments.
|
||||
DB::transaction(function () use ($accessory, $request, $target, &$payload): void {
|
||||
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
|
||||
|
||||
$accessory_checkout = new AccessoryCheckout([
|
||||
'accessory_id' => $accessory->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_to' => $target->id,
|
||||
'assigned_type' => $target::class,
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
|
||||
$accessory_checkout->created_by = auth()->id();
|
||||
$accessory_checkout->save();
|
||||
|
||||
$payload = [
|
||||
'accessory_id' => $accessory->id,
|
||||
'assigned_to' => $target->id,
|
||||
'assigned_type' => $target::class,
|
||||
'note' => $request->input('note'),
|
||||
'created_by' => auth()->id(),
|
||||
'pivot' => $accessory_checkout->id,
|
||||
];
|
||||
}
|
||||
|
||||
// Set this value to be able to pass the qty through to the event.
|
||||
event(new CheckoutableCheckedOut(
|
||||
$accessory,
|
||||
$target,
|
||||
auth()->user(),
|
||||
$request->input('note'),
|
||||
[],
|
||||
$accessory->checkout_qty,
|
||||
));
|
||||
});
|
||||
// Set this value to be able to pass the qty through to the event
|
||||
event(new CheckoutableCheckedOut(
|
||||
$accessory,
|
||||
$target,
|
||||
auth()->user(),
|
||||
$request->input('note'),
|
||||
[],
|
||||
$accessory->checkout_qty,
|
||||
));
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkout.success')));
|
||||
|
||||
|
||||
@@ -706,35 +706,18 @@ class AssetsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$target = $this->resolveCheckoutTargetForAssetMutation($request);
|
||||
$requestedCheckout = $request->filled('assigned_user') || $request->filled('assigned_asset') || $request->filled('assigned_location');
|
||||
|
||||
if ($requestedCheckout && (! $target)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')));
|
||||
}
|
||||
|
||||
if ($requestedCheckout) {
|
||||
$companyMismatchResponse = $this->checkoutCompanyMismatchResponse($asset, $target);
|
||||
if ($companyMismatchResponse) {
|
||||
return $companyMismatchResponse;
|
||||
if ($asset->save()) {
|
||||
if ($request->input('assigned_user')) {
|
||||
$target = User::find(request('assigned_user'));
|
||||
} elseif ($request->input('assigned_asset')) {
|
||||
$target = Asset::find(request('assigned_asset'));
|
||||
} elseif ($request->input('assigned_location')) {
|
||||
$target = Location::find(request('assigned_location'));
|
||||
}
|
||||
}
|
||||
|
||||
$stored = DB::transaction(function () use ($asset, $request, $target, $requestedCheckout): bool {
|
||||
if (! $asset->save()) {
|
||||
return false;
|
||||
if (isset($target)) {
|
||||
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset creation', e($request->input('name')));
|
||||
}
|
||||
|
||||
if ($requestedCheckout) {
|
||||
// Keep create + optional checkout side effects atomic.
|
||||
return $asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset creation', e($request->input('name')));
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if ($stored) {
|
||||
|
||||
if ($asset->image) {
|
||||
$asset->image = $asset->getImageUrl();
|
||||
}
|
||||
@@ -809,54 +792,25 @@ class AssetsController extends Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
$target = $this->resolveCheckoutTargetForAssetMutation($request, $asset->id);
|
||||
$requestedCheckout = $request->filled('assigned_user') || $request->filled('assigned_asset') || $request->filled('assigned_location');
|
||||
if ($asset->save()) {
|
||||
if (($request->filled('assigned_user')) && ($target = User::find($request->input('assigned_user')))) {
|
||||
$location = $target->location_id;
|
||||
} elseif (($request->filled('assigned_asset')) && ($target = Asset::find($request->input('assigned_asset')))) {
|
||||
$location = $target->location_id;
|
||||
|
||||
if ($requestedCheckout && (! $target)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')));
|
||||
}
|
||||
|
||||
if ($requestedCheckout) {
|
||||
$companyMismatchResponse = $this->checkoutCompanyMismatchResponse($asset, $target);
|
||||
if ($companyMismatchResponse) {
|
||||
return $companyMismatchResponse;
|
||||
}
|
||||
}
|
||||
|
||||
$updated = DB::transaction(function () use ($asset, $request, $target, $requestedCheckout): bool {
|
||||
if (! $asset->save()) {
|
||||
return false;
|
||||
Asset::where('assigned_type', Asset::class)->where('assigned_to', $asset->id)
|
||||
->update(['location_id' => $target->location_id]);
|
||||
} elseif (($request->filled('assigned_location')) && ($target = Location::find($request->input('assigned_location')))) {
|
||||
$location = $target->id;
|
||||
}
|
||||
|
||||
if ($requestedCheckout) {
|
||||
if (isset($target)) {
|
||||
// Using `->has` preserves the asset name if the name parameter was not included in request.
|
||||
$asset_name = request()->has('name') ? request('name') : $asset->name;
|
||||
|
||||
$location = null;
|
||||
if ($request->filled('assigned_user')) {
|
||||
$location = $target->location_id;
|
||||
} elseif ($request->filled('assigned_asset')) {
|
||||
$location = $target->location_id;
|
||||
} elseif ($request->filled('assigned_location')) {
|
||||
$location = $target->id;
|
||||
}
|
||||
|
||||
// Keep update + optional checkout side effects atomic.
|
||||
if (! $asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update', $asset_name, $location)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($request->filled('assigned_asset')) {
|
||||
Asset::where('assigned_type', Asset::class)->where('assigned_to', $asset->id)
|
||||
->update(['location_id' => $target->location_id]);
|
||||
}
|
||||
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update', $asset_name, $location);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if ($updated) {
|
||||
|
||||
if ($asset->image) {
|
||||
$asset->image = $asset->getImageUrl();
|
||||
}
|
||||
@@ -875,36 +829,6 @@ class AssetsController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
|
||||
}
|
||||
|
||||
private function resolveCheckoutTargetForAssetMutation(Request $request, ?int $assetId = null): User|Asset|Location|null
|
||||
{
|
||||
if ($request->filled('assigned_user')) {
|
||||
return User::withoutGlobalScopes()->find($request->input('assigned_user'));
|
||||
}
|
||||
|
||||
if ($request->filled('assigned_asset')) {
|
||||
return Asset::withoutGlobalScopes()->where('id', '!=', $assetId)->find($request->input('assigned_asset'));
|
||||
}
|
||||
|
||||
if ($request->filled('assigned_location')) {
|
||||
return Location::withoutGlobalScopes()->find($request->input('assigned_location'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function checkoutCompanyMismatchResponse(Asset $asset, User|Asset|Location $target): ?JsonResponse
|
||||
{
|
||||
if ((Setting::getSettings()->full_multiple_companies_support == '1')
|
||||
&& (! is_null($asset->company_id))
|
||||
&& (! is_null($target->company_id))
|
||||
&& ((int) $asset->company_id !== (int) $target->company_id)
|
||||
) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a given asset (mark as deleted).
|
||||
*
|
||||
@@ -981,7 +905,6 @@ class AssetsController extends Controller
|
||||
*/
|
||||
public function checkoutByTag(AssetCheckoutRequest $request, $tag): JsonResponse
|
||||
{
|
||||
// Use the same hardened checkout path as ID-based checkout.
|
||||
if ($asset = Asset::where('asset_tag', $tag)->first()) {
|
||||
return $this->checkout($request, $asset->id);
|
||||
}
|
||||
@@ -1017,22 +940,19 @@ class AssetsController extends Controller
|
||||
|
||||
// This item is checked out to a location
|
||||
if (request('checkout_to_type') == 'location') {
|
||||
// Resolve unscoped target first so FMCS mismatch can be handled explicitly.
|
||||
$target = Location::withoutGlobalScopes()->find(request('assigned_location'));
|
||||
$target = Location::find(request('assigned_location'));
|
||||
$asset->location_id = ($target) ? $target->id : '';
|
||||
$error_payload['target_id'] = $request->input('assigned_location');
|
||||
$error_payload['target_type'] = 'location';
|
||||
} elseif (request('checkout_to_type') == 'asset') {
|
||||
// Resolve unscoped target first so FMCS mismatch can be handled explicitly.
|
||||
$target = Asset::withoutGlobalScopes()->where('id', '!=', $asset_id)->find(request('assigned_asset'));
|
||||
$target = Asset::where('id', '!=', $asset_id)->find(request('assigned_asset'));
|
||||
// Override with the asset's location_id if it has one
|
||||
$asset->location_id = (($target) && (isset($target->location_id))) ? $target->location_id : '';
|
||||
$error_payload['target_id'] = $request->input('assigned_asset');
|
||||
$error_payload['target_type'] = 'asset';
|
||||
} elseif (request('checkout_to_type') == 'user') {
|
||||
// Fetch the target and set the asset's new location_id
|
||||
// Resolve unscoped target first so FMCS mismatch can be handled explicitly.
|
||||
$target = User::withoutGlobalScopes()->find(request('assigned_user'));
|
||||
$target = User::find(request('assigned_user'));
|
||||
$asset->location_id = (($target) && (isset($target->location_id))) ? $target->location_id : '';
|
||||
$error_payload['target_id'] = $request->input('assigned_user');
|
||||
$error_payload['target_type'] = 'user';
|
||||
@@ -1051,16 +971,6 @@ class AssetsController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('error', $error_payload, 'Checkout target for asset '.e($asset->asset_tag).' is invalid - '.$error_payload['target_type'].' does not exist.'));
|
||||
}
|
||||
|
||||
// In FMCS mode, enforce explicit same-company target checks before mutating checkout state.
|
||||
$targetCompanyId = data_get($target, 'company_id');
|
||||
if ((Setting::getSettings()->full_multiple_companies_support == '1')
|
||||
&& (! is_null($asset->company_id))
|
||||
&& (! is_null($targetCompanyId))
|
||||
&& ((int) $asset->company_id !== (int) $targetCompanyId)
|
||||
) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', $error_payload, trans('general.error_user_company')));
|
||||
}
|
||||
|
||||
$checkout_at = request('checkout_at', date('Y-m-d H:i:s'));
|
||||
$expected_checkin = request('expected_checkin', null);
|
||||
$note = request('note', null);
|
||||
@@ -1075,12 +985,7 @@ class AssetsController extends Controller
|
||||
// $asset->location_id = $target->rtd_location_id;
|
||||
// }
|
||||
|
||||
// Keep checkout mutation + checkout logging/event side effects atomic.
|
||||
$wasCheckedOut = DB::transaction(function () use ($asset, $target, $checkout_at, $expected_checkin, $note, $asset_name): bool {
|
||||
return $asset->checkOut($target, auth()->user(), $checkout_at, $expected_checkin, $note, $asset_name, $asset->location_id);
|
||||
});
|
||||
|
||||
if ($wasCheckedOut) {
|
||||
if ($asset->checkOut($target, auth()->user(), $checkout_at, $expected_checkin, $note, $asset_name, $asset->location_id)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', ['asset' => e($asset->asset_tag)], trans('admin/hardware/message.checkout.success')));
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Http\Transformers\ComponentsTransformer;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Company;
|
||||
use App\Models\Component;
|
||||
use App\Models\Setting;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -315,33 +314,20 @@ class ComponentsController extends Controller
|
||||
}
|
||||
|
||||
if ($component->numRemaining() >= $request->input('assigned_qty')) {
|
||||
// Resolve the raw target first, then enforce FMCS explicitly.
|
||||
// Scoped lookup can hide cross-company records and lead to partial writes.
|
||||
$asset = Asset::withoutGlobalScopes()->find($request->input('assigned_to'));
|
||||
|
||||
if (! $asset) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')));
|
||||
}
|
||||
$asset = Asset::find($request->input('assigned_to'));
|
||||
$component->assigned_to = $request->input('assigned_to');
|
||||
|
||||
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($component->company_id !== $asset->company_id)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||
}
|
||||
$component->assets()->attach($component->id, [
|
||||
'component_id' => $component->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_qty' => $request->input('assigned_qty', 1),
|
||||
'created_by' => auth()->id(),
|
||||
'asset_id' => $request->input('assigned_to'),
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
|
||||
// Keep pivot + action log in one transaction so checkout is all-or-nothing.
|
||||
DB::transaction(function () use ($component, $request, $asset): void {
|
||||
$component->assigned_to = $request->input('assigned_to');
|
||||
|
||||
$component->assets()->attach($component->id, [
|
||||
'component_id' => $component->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_qty' => $request->input('assigned_qty', 1),
|
||||
'created_by' => auth()->id(),
|
||||
'asset_id' => $request->input('assigned_to'),
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
|
||||
$component->logCheckout($request->input('note'), $asset, null, [], $request->get('assigned_qty', 1));
|
||||
});
|
||||
$component->logCheckout($request->input('note'), $asset, null, [], $request->get('assigned_qty', 1));
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.checkout.success')));
|
||||
}
|
||||
|
||||
@@ -13,11 +13,9 @@ use App\Http\Transformers\ConsumablesTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Models\Company;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ConsumablesController extends Controller
|
||||
{
|
||||
@@ -308,42 +306,34 @@ class ConsumablesController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/consumables/message.checkout.unavailable', ['requested' => $consumable->checkout_qty, 'remaining' => $consumable->numRemaining()])));
|
||||
}
|
||||
|
||||
// Resolve the raw target first, then enforce FMCS explicitly.
|
||||
// Scoped lookup can hide cross-company users and make failures ambiguous.
|
||||
if (! $user = User::withoutGlobalScopes()->find($request->input('assigned_to'))) {
|
||||
// Check if the user exists - @TODO: this should probably be handled via validation, not here??
|
||||
if (! $user = User::find($request->input('assigned_to'))) {
|
||||
// Return error message
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'No user found'));
|
||||
}
|
||||
|
||||
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($consumable->company_id !== $user->company_id)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||
}
|
||||
|
||||
// Update the consumable data
|
||||
$consumable->assigned_to = $request->input('assigned_to');
|
||||
|
||||
// Keep pivot writes and checkout log/event atomic to avoid partial checkout state.
|
||||
DB::transaction(function () use ($consumable, $request, $user): void {
|
||||
for ($i = 0; $i < $consumable->checkout_qty; $i++) {
|
||||
$consumable->users()->attach($consumable->id,
|
||||
[
|
||||
'consumable_id' => $consumable->id,
|
||||
'created_by' => $user->id,
|
||||
'assigned_to' => $request->input('assigned_to'),
|
||||
'note' => $request->input('note'),
|
||||
]
|
||||
);
|
||||
}
|
||||
for ($i = 0; $i < $consumable->checkout_qty; $i++) {
|
||||
$consumable->users()->attach($consumable->id,
|
||||
[
|
||||
'consumable_id' => $consumable->id,
|
||||
'created_by' => $user->id,
|
||||
'assigned_to' => $request->input('assigned_to'),
|
||||
'note' => $request->input('note'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
event(new CheckoutableCheckedOut(
|
||||
$consumable,
|
||||
$user,
|
||||
auth()->user(),
|
||||
$request->input('note'),
|
||||
[],
|
||||
$consumable->checkout_qty,
|
||||
));
|
||||
});
|
||||
event(new CheckoutableCheckedOut(
|
||||
$consumable,
|
||||
$user,
|
||||
auth()->user(),
|
||||
$request->input('note'),
|
||||
[],
|
||||
$consumable->checkout_qty,
|
||||
));
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/consumables/message.checkout.success')));
|
||||
|
||||
|
||||
@@ -8,11 +8,9 @@ use App\Http\Transformers\LicenseSeatsTransformer;
|
||||
use App\Models\Asset;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class LicenseSeatsController extends Controller
|
||||
{
|
||||
@@ -108,8 +106,7 @@ class LicenseSeatsController extends Controller
|
||||
'prohibits:asset_id',
|
||||
// must be a valid user or null to unassign
|
||||
function ($attribute, $value, $fail) {
|
||||
// Validate existence without company scopes; FMCS checks happen explicitly below.
|
||||
if (! is_null($value) && ! User::withoutGlobalScopes()->where('id', $value)->whereNull('deleted_at')->exists()) {
|
||||
if (! is_null($value) && ! User::where('id', $value)->whereNull('deleted_at')->exists()) {
|
||||
$fail('The selected assigned_to is invalid.');
|
||||
}
|
||||
},
|
||||
@@ -121,8 +118,7 @@ class LicenseSeatsController extends Controller
|
||||
'prohibits:assigned_to',
|
||||
// must be a valid asset or null to unassign
|
||||
function ($attribute, $value, $fail) {
|
||||
// Validate existence without company scopes; FMCS checks happen explicitly below.
|
||||
if (! is_null($value) && ! Asset::withoutGlobalScopes()->where('id', $value)->whereNull('deleted_at')->exists()) {
|
||||
if (! is_null($value) && ! Asset::where('id', $value)->whereNull('deleted_at')->exists()) {
|
||||
$fail('The selected asset_id is invalid.');
|
||||
}
|
||||
},
|
||||
@@ -143,34 +139,6 @@ class LicenseSeatsController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat does not belong to the specified license'));
|
||||
}
|
||||
|
||||
$targetUser = null;
|
||||
if (! is_null($request->input('assigned_to'))) {
|
||||
// Resolve unscoped target so we can return a clean cross-company error instead of a hidden-not-found.
|
||||
$targetUser = User::withoutGlobalScopes()->find($request->input('assigned_to'));
|
||||
|
||||
if (! $targetUser) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
|
||||
}
|
||||
|
||||
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($license->company_id !== $targetUser->company_id)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||
}
|
||||
}
|
||||
|
||||
$targetAsset = null;
|
||||
if (! is_null($request->input('asset_id'))) {
|
||||
// Resolve unscoped target so FMCS company mismatch can be enforced explicitly.
|
||||
$targetAsset = Asset::withoutGlobalScopes()->find($request->input('asset_id'));
|
||||
|
||||
if (! $targetAsset) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
|
||||
}
|
||||
|
||||
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($license->company_id !== $targetAsset->company_id)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||
}
|
||||
}
|
||||
|
||||
$oldUser = $licenseSeat->user;
|
||||
$oldAsset = $licenseSeat->asset;
|
||||
|
||||
@@ -198,11 +166,11 @@ class LicenseSeatsController extends Controller
|
||||
// the logging functions expect only one "target". if both asset and user are present in the request,
|
||||
// we simply let assets take precedence over users...
|
||||
if ($licenseSeat->isDirty('assigned_to')) {
|
||||
$target = $is_checkin ? $oldUser : $targetUser;
|
||||
$target = $is_checkin ? $oldUser : User::find($licenseSeat->assigned_to);
|
||||
}
|
||||
|
||||
if ($licenseSeat->isDirty('asset_id')) {
|
||||
$target = $is_checkin ? $oldAsset : $targetAsset;
|
||||
$target = $is_checkin ? $oldAsset : Asset::find($licenseSeat->asset_id);
|
||||
}
|
||||
|
||||
if ($assignmentTouched && is_null($target)) {
|
||||
@@ -213,22 +181,13 @@ class LicenseSeatsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Keep seat updates and checkout/checkin logging atomic to prevent partial state changes.
|
||||
$updated = DB::transaction(function () use ($licenseSeat, $assignmentTouched, $is_checkin, $target, $request): bool {
|
||||
if (! $licenseSeat->save()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($licenseSeat->save()) {
|
||||
if ($assignmentTouched) {
|
||||
if ($is_checkin) {
|
||||
if (! $licenseSeat->license->reassignable) {
|
||||
$licenseSeat->unreassignable_seat = true;
|
||||
|
||||
if (! $licenseSeat->save()) {
|
||||
return false;
|
||||
}
|
||||
$licenseSeat->save();
|
||||
}
|
||||
|
||||
// todo: skip if target is null?
|
||||
$licenseSeat->logCheckin($target, $licenseSeat->notes);
|
||||
} else {
|
||||
@@ -237,10 +196,6 @@ class LicenseSeatsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if ($updated) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class LicensesController extends Controller
|
||||
{
|
||||
$this->authorize('view', License::class);
|
||||
|
||||
$licenses = License::with('company', 'manufacturer', 'supplier', 'category', 'adminuser', 'licenseSeatsRelation', 'assignedCount')->withCount('freeSeats as free_seats_count');
|
||||
$licenses = License::with('company', 'manufacturer', 'supplier', 'category', 'adminuser')->withCount('freeSeats as free_seats_count');
|
||||
$settings = Setting::getSettings();
|
||||
|
||||
if ($request->input('status') == 'inactive') {
|
||||
@@ -247,7 +247,7 @@ class LicensesController extends Controller
|
||||
if ($license->assigned_seats_count == 0) {
|
||||
// Delete the license and the associated license seats
|
||||
DB::table('license_seats')
|
||||
->where('license_id', $license->id)
|
||||
->where('id', $license->id)
|
||||
->update(['assigned_to' => null, 'asset_id' => null]);
|
||||
|
||||
$licenseSeats = $license->licenseseats();
|
||||
|
||||
@@ -6,18 +6,8 @@ use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\FilterRequest;
|
||||
use App\Http\Transformers\ActionlogsTransformer;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Component;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\Maintenance;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class ReportsController extends Controller
|
||||
@@ -135,141 +125,4 @@ class ReportsController extends Controller
|
||||
return response()->json((new ActionlogsTransformer)->transformActionlogs($actionlogs, $total), 200, ['Content-Type' => 'application/json;charset=utf8'], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns time-series data for the reports overview charts.
|
||||
*
|
||||
* Accepts ?days=N (preset, default 30) OR ?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD.
|
||||
* Also returns the immediately preceding period of equal length for comparison lines.
|
||||
*/
|
||||
public function activityChart(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('reports.view');
|
||||
|
||||
$allowedDays = [7, 14, 30, 60, 90, 180, 365];
|
||||
|
||||
if ($request->filled('start_date') && $request->filled('end_date')) {
|
||||
$curStart = Carbon::parse($request->input('start_date'))->startOfDay();
|
||||
$curEnd = Carbon::parse($request->input('end_date'))->endOfDay();
|
||||
if ($curEnd->lt($curStart)) {
|
||||
[$curStart, $curEnd] = [$curEnd, $curStart];
|
||||
}
|
||||
$days = max(1, (int) $curStart->diffInDays($curEnd) + 1);
|
||||
} else {
|
||||
$days = in_array((int) $request->input('days'), $allowedDays) ? (int) $request->input('days') : 30;
|
||||
$curEnd = Carbon::today()->endOfDay();
|
||||
$curStart = Carbon::today()->subDays($days - 1)->startOfDay();
|
||||
}
|
||||
|
||||
$prevEnd = $curStart->copy()->subSecond()->endOfDay();
|
||||
$prevStart = $prevEnd->copy()->subDays($days - 1)->startOfDay();
|
||||
|
||||
$buildDates = function (Carbon $start, Carbon $end): array {
|
||||
$dates = [];
|
||||
for ($d = $start->copy(); $d->lte($end); $d->addDay()) {
|
||||
$dates[] = $d->toDateString();
|
||||
}
|
||||
|
||||
return $dates;
|
||||
};
|
||||
|
||||
$curDates = $buildDates($curStart, $curEnd);
|
||||
$prevDates = $buildDates($prevStart, $prevEnd);
|
||||
|
||||
$pluckAction = function (string $actionType, Carbon $start, Carbon $end): array {
|
||||
return Actionlog::where('action_type', $actionType)
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date')
|
||||
->toArray();
|
||||
};
|
||||
|
||||
// withTrashed() ensures records deleted after creation still appear in their creation-period counts.
|
||||
$pluckCreated = function (string $modelClass, Carbon $start, Carbon $end): array {
|
||||
return $modelClass::withTrashed()
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date')
|
||||
->toArray();
|
||||
};
|
||||
|
||||
// Maintenance has no company_id column and no CompanyableTrait, so scope through
|
||||
// its asset relationship — whereHas('asset') applies Asset's FMCS global scope.
|
||||
$pluckMaintenances = function (Carbon $start, Carbon $end): array {
|
||||
return Maintenance::withTrashed()
|
||||
->whereHas('asset')
|
||||
->whereBetween('maintenances.created_at', [$start, $end])
|
||||
->selectRaw('DATE(maintenances.created_at) as date, COUNT(*) as count')
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date')
|
||||
->toArray();
|
||||
};
|
||||
|
||||
// Filters by both action_type and item_type for per-category checkout/checkin counts.
|
||||
$pluckActionByType = function (string $actionType, string $modelClass, Carbon $start, Carbon $end): array {
|
||||
return Actionlog::where('action_type', $actionType)
|
||||
->where('item_type', $modelClass)
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date')
|
||||
->toArray();
|
||||
};
|
||||
|
||||
$pluckDeletedUsers = function (Carbon $start, Carbon $end): array {
|
||||
return User::withTrashed()
|
||||
->whereNotNull('deleted_at')
|
||||
->whereBetween('deleted_at', [$start, $end])
|
||||
->selectRaw('DATE(deleted_at) as date, COUNT(*) as count')
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date')
|
||||
->toArray();
|
||||
};
|
||||
|
||||
// Catches both 'checkin' and 'checkin from' action types used across different item types.
|
||||
$pluckCheckinsByType = function (string $modelClass, Carbon $start, Carbon $end): array {
|
||||
return Actionlog::whereIn('action_type', ['checkin', 'checkin from'])
|
||||
->where('item_type', $modelClass)
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date')
|
||||
->toArray();
|
||||
};
|
||||
|
||||
$fill = fn (array $raw, array $dates) => array_map(fn ($d) => (int) ($raw[$d] ?? 0), $dates);
|
||||
|
||||
$datasets = [];
|
||||
foreach ([
|
||||
'new_users' => fn ($s, $e) => $pluckCreated(User::class, $s, $e),
|
||||
'deleted_users' => fn ($s, $e) => $pluckDeletedUsers($s, $e),
|
||||
'asset_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', Asset::class, $s, $e),
|
||||
'asset_checkins' => fn ($s, $e) => $pluckCheckinsByType(Asset::class, $s, $e),
|
||||
'new_assets' => fn ($s, $e) => $pluckCreated(Asset::class, $s, $e),
|
||||
'new_maintenances' => fn ($s, $e) => $pluckMaintenances($s, $e),
|
||||
'new_audits' => fn ($s, $e) => $pluckAction('audit', $s, $e),
|
||||
'component_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', Component::class, $s, $e),
|
||||
'component_checkins' => fn ($s, $e) => $pluckCheckinsByType(Component::class, $s, $e),
|
||||
'new_components' => fn ($s, $e) => $pluckCreated(Component::class, $s, $e),
|
||||
'consumable_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', Consumable::class, $s, $e),
|
||||
'consumable_checkins' => fn ($s, $e) => $pluckCheckinsByType(Consumable::class, $s, $e),
|
||||
'new_consumables' => fn ($s, $e) => $pluckCreated(Consumable::class, $s, $e),
|
||||
'license_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', LicenseSeat::class, $s, $e),
|
||||
'license_checkins' => fn ($s, $e) => $pluckCheckinsByType(LicenseSeat::class, $s, $e),
|
||||
'new_licenses' => fn ($s, $e) => $pluckCreated(License::class, $s, $e),
|
||||
'accessory_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', Accessory::class, $s, $e),
|
||||
'accessory_checkins' => fn ($s, $e) => $pluckCheckinsByType(Accessory::class, $s, $e),
|
||||
'new_accessories' => fn ($s, $e) => $pluckCreated(Accessory::class, $s, $e),
|
||||
] as $key => $query) {
|
||||
$datasets[$key] = $fill($query($curStart, $curEnd), $curDates);
|
||||
$datasets['prev_'.$key] = $fill($query($prevStart, $prevEnd), $prevDates);
|
||||
}
|
||||
|
||||
return response()->json(array_merge([
|
||||
'labels' => array_map(fn ($d) => Carbon::parse($d)->format('M j'), $curDates),
|
||||
'prev_label' => $prevStart->format('M j').' – '.$prevEnd->format('M j'),
|
||||
], $datasets));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Licenses;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\License;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class BulkLicensesController extends Controller
|
||||
{
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$this->authorize('delete', License::class);
|
||||
|
||||
$errors = [];
|
||||
$success_count = 0;
|
||||
|
||||
foreach ($request->ids as $id) {
|
||||
$license = License::find($id);
|
||||
|
||||
if (is_null($license)) {
|
||||
$errors[] = trans('admin/licenses/message.does_not_exist');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $license)) {
|
||||
$errors[] = trans('general.insufficient_permissions');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($license->assigned_seats_count > 0) {
|
||||
$errors[] = trans('admin/licenses/message.delete.bulk_checkout_warning', ['license_name' => $license->name]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Since assigned_seats_count == 0, all seats already have assigned_to and asset_id as null,
|
||||
// so this update is effectively a no-op. It mirrors the single destroy() and is kept as a
|
||||
// safety net. Bypassing Eloquent events here is intentional and safe — there is nothing
|
||||
// assigned to trigger events on. Prior checkout/checkin history is preserved in action_log
|
||||
// (keyed by LicenseSeat item_type/item_id) and remains accessible even after soft-delete.
|
||||
DB::table('license_seats')
|
||||
->where('license_id', $license->id)
|
||||
->update(['assigned_to' => null, 'asset_id' => null]);
|
||||
|
||||
$license->licenseseats()->delete();
|
||||
$license->delete();
|
||||
$success_count++;
|
||||
}
|
||||
|
||||
if (count($errors) > 0) {
|
||||
if ($success_count > 0) {
|
||||
return redirect()->route('licenses.index')
|
||||
->with('success', trans_choice('admin/licenses/message.delete.partial_success', $success_count, ['count' => $success_count]))
|
||||
->with('multi_error_messages', $errors);
|
||||
}
|
||||
|
||||
return redirect()->route('licenses.index')->with('multi_error_messages', $errors);
|
||||
}
|
||||
|
||||
return redirect()->route('licenses.index')->with('success', trans('admin/licenses/message.delete.bulk_success'));
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use League\Csv\EscapeFormula;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
@@ -389,8 +388,6 @@ class LicensesController extends Controller
|
||||
|
||||
fputcsv($handle, $headers);
|
||||
|
||||
$formatter = new EscapeFormula('`');
|
||||
|
||||
foreach ($licenses as $license) {
|
||||
// Add a new row with data
|
||||
$values = [
|
||||
@@ -422,14 +419,7 @@ class LicensesController extends Controller
|
||||
$license->created_at,
|
||||
];
|
||||
|
||||
// CSV_ESCAPE_FORMULAS is set to false in the .env
|
||||
if (config('app.escape_formulas') === false) {
|
||||
fputcsv($handle, $values);
|
||||
|
||||
// CSV_ESCAPE_FORMULAS is set to true or is not set in the .env
|
||||
} else {
|
||||
fputcsv($handle, $formatter->escapeRecord($values));
|
||||
}
|
||||
fputcsv($handle, $values);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -211,19 +211,14 @@ class ProfileController extends Controller
|
||||
*/
|
||||
public function printInventory(): View
|
||||
{
|
||||
$userId = auth()->id();
|
||||
$show_users = User::where('id', auth()->user()->id)->get();
|
||||
|
||||
$show_user = User::withInventoryRelations($userId)->first();
|
||||
|
||||
$indirectItemsCount =
|
||||
$show_user->assets->flatMap->assignedAssets->count()
|
||||
+ $show_user->assets->flatMap->components->count()
|
||||
+ $show_user->assets->flatMap->licenses->count()
|
||||
+ $show_user->assets->flatMap->assignedAccessories->count();
|
||||
|
||||
return view('users.print')
|
||||
->with('users', [$show_user])
|
||||
->with('indirectItemsCount', $indirectItemsCount)
|
||||
return view('users/print')
|
||||
->with('assets', auth()->user()->assets())
|
||||
->with('licenses', auth()->user()->licenses()->get())
|
||||
->with('accessories', auth()->user()->accessories()->get())
|
||||
->with('consumables', auth()->user()->consumables()->get())
|
||||
->with('users', $show_users)
|
||||
->with('settings', Setting::getSettings());
|
||||
}
|
||||
|
||||
|
||||
@@ -56,31 +56,6 @@ class ReportsController extends Controller
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
$this->authorize('reports.view');
|
||||
$settings = Setting::getSettings();
|
||||
|
||||
$audit_alert_count = Asset::DueOrOverdueForAudit($settings)->count();
|
||||
$checkin_alert_count = Asset::DueOrOverdueForCheckin($settings)->count();
|
||||
// CheckoutAcceptance has no company_id column; scope through the checkoutable
|
||||
// relationship so each type's CompanyableTrait global scope is applied.
|
||||
$pending_acceptance_count = CheckoutAcceptance::pending()
|
||||
->whereHasMorph('checkoutable', [Asset::class, LicenseSeat::class, Accessory::class, Component::class, Consumable::class])
|
||||
->count();
|
||||
$licenses_low_count = License::withCount(['freeSeats as free_seats_count'])
|
||||
->get()
|
||||
->filter(fn ($l) => $l->free_seats_count <= 0)
|
||||
->count();
|
||||
|
||||
return view('reports/index', compact(
|
||||
'audit_alert_count',
|
||||
'checkin_alert_count',
|
||||
'pending_acceptance_count',
|
||||
'licenses_low_count',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a view that displays the accessories report.
|
||||
*
|
||||
@@ -277,7 +252,6 @@ class ReportsController extends Controller
|
||||
|
||||
$response = new StreamedResponse(function () {
|
||||
Log::debug('Starting streamed response');
|
||||
Log::debug('CSV escaping is set to: '.config('app.escape_formulas'));
|
||||
|
||||
// Open output stream
|
||||
$handle = fopen('php://output', 'w');
|
||||
@@ -313,8 +287,6 @@ class ReportsController extends Controller
|
||||
Log::debug('Walking results: '.$executionTime);
|
||||
$count = 0;
|
||||
|
||||
$formatter = new EscapeFormula('`');
|
||||
|
||||
foreach ($actionlogs as $actionlog) {
|
||||
$count++;
|
||||
$target_name = '';
|
||||
@@ -345,15 +317,7 @@ class ReportsController extends Controller
|
||||
$actionlog->action_source,
|
||||
$actionlog->log_meta,
|
||||
];
|
||||
|
||||
// CSV_ESCAPE_FORMULAS is set to false in the .env
|
||||
if (config('app.escape_formulas') === false) {
|
||||
fputcsv($handle, $row);
|
||||
|
||||
// CSV_ESCAPE_FORMULAS is set to true or is not set in the .env
|
||||
} else {
|
||||
fputcsv($handle, $formatter->escapeRecord($row));
|
||||
}
|
||||
fputcsv($handle, $row);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -888,7 +852,7 @@ class ReportsController extends Controller
|
||||
}
|
||||
|
||||
if ($request->filled('purchase_date')) {
|
||||
$row[] = ($asset->purchase_date) ? Carbon::parse($asset->purchase_date)->format('Y-m-d') : '';
|
||||
$row[] = ($asset->purchase_date) ? $asset->purchase_date : '';
|
||||
}
|
||||
|
||||
if ($request->filled('purchase_cost')) {
|
||||
@@ -896,7 +860,7 @@ class ReportsController extends Controller
|
||||
}
|
||||
|
||||
if ($request->filled('eol')) {
|
||||
$row[] = ($asset->asset_eol_date != '') ? Carbon::parse($asset->asset_eol_date)->format('Y-m-d') : '';
|
||||
$row[] = ($asset->asset_eol_date != '') ? $asset->asset_eol_date : '';
|
||||
}
|
||||
|
||||
if ($request->filled('warranty')) {
|
||||
|
||||
@@ -26,7 +26,6 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use League\Csv\EscapeFormula;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
@@ -573,8 +572,6 @@ class UsersController extends Controller
|
||||
|
||||
fputcsv($handle, $headers);
|
||||
|
||||
$formatter = new EscapeFormula('`');
|
||||
|
||||
foreach ($users as $user) {
|
||||
$user_groups = '';
|
||||
|
||||
@@ -617,14 +614,7 @@ class UsersController extends Controller
|
||||
$user->created_at,
|
||||
];
|
||||
|
||||
// CSV_ESCAPE_FORMULAS is set to false in the .env
|
||||
if (config('app.escape_formulas') === false) {
|
||||
fputcsv($handle, $values);
|
||||
|
||||
// CSV_ESCAPE_FORMULAS is set to true or is not set in the .env
|
||||
} else {
|
||||
fputcsv($handle, $formatter->escapeRecord($values));
|
||||
}
|
||||
fputcsv($handle, $values);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -649,16 +639,32 @@ class UsersController extends Controller
|
||||
{
|
||||
$this->authorize('view', User::class);
|
||||
|
||||
$user = User::withInventoryRelations($id)->first();
|
||||
|
||||
$indirectItemsCount = $user?->assets?->flatMap->assignedAssets->count() + $user?->assets?->flatMap->components->count() + $user?->assets?->flatMap->licenses->count() + $user?->assets?->flatMap->assignedAccessories->count();
|
||||
$user = User::where('id', $id)
|
||||
->with([
|
||||
'assets.log' => fn ($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
|
||||
'assets.assignedAssets.log' => fn ($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
|
||||
'assets.assignedAssets.defaultLoc',
|
||||
'assets.assignedAssets.location',
|
||||
'assets.assignedAssets.model.category',
|
||||
'assets.defaultLoc',
|
||||
'assets.location',
|
||||
'assets.model.category',
|
||||
'accessories.log' => fn ($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
|
||||
'accessories.category',
|
||||
'accessories.manufacturer',
|
||||
'consumables.log' => fn ($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
|
||||
'consumables.category',
|
||||
'consumables.manufacturer',
|
||||
'licenses.category',
|
||||
])
|
||||
->withTrashed()
|
||||
->first();
|
||||
|
||||
if ($user) {
|
||||
$this->authorize('view', $user);
|
||||
|
||||
return view('users.print')
|
||||
->with('users', [$user])
|
||||
->with('indirectItemsCount', $indirectItemsCount)
|
||||
->with('settings', Setting::getSettings());
|
||||
}
|
||||
|
||||
|
||||
@@ -75,10 +75,7 @@ class LicensesTransformer
|
||||
'checkin' => Gate::allows('checkin', License::class),
|
||||
'clone' => Gate::allows('create', License::class),
|
||||
'update' => Gate::allows('update', License::class),
|
||||
'delete' => $license->isDeletable(),
|
||||
'bulk_selectable' => [
|
||||
'delete' => $license->isDeletable(),
|
||||
],
|
||||
'delete' => (Gate::allows('delete', License::class) && ($license->free_seats_count == $license->seats)) ? true : false,
|
||||
];
|
||||
|
||||
$array += $permissions_array;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('audit_location')]
|
||||
#[Title('Audit Location')]
|
||||
#[Description('Review all assets at a location, flag overdue audits and status anomalies')]
|
||||
class AuditLocationPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$location = $request->get('location');
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are conducting an asset audit for location: {$location}.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. Find the location record for "{$location}" (search by name if needed).
|
||||
2. List all assets currently assigned to or located at that location.
|
||||
3. Identify any assets with overdue audit dates (next_audit_date is in the past).
|
||||
4. Flag any assets with unexpected status labels (e.g. archived, pending, or out-for-repair assets that appear to still be at this location).
|
||||
5. Note any assets that have been at this location longer than expected without a check-in or audit event.
|
||||
6. Produce a summary report with: total asset count, assets requiring audit, assets with status anomalies, and any recommended actions.
|
||||
|
||||
Present the findings clearly so they can be acted on or exported.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('location', 'Name or ID of the location to audit', required: true),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('end_of_life_review')]
|
||||
#[Title('End of Life Review')]
|
||||
#[Description('Identify assets that have passed their EOL date or are fully depreciated, and recommend disposition actions')]
|
||||
class EndOfLifeReviewPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$department = $request->get('department');
|
||||
$category = $request->get('category');
|
||||
|
||||
$scope = collect([
|
||||
$department ? "department: {$department}" : null,
|
||||
$category ? "category: {$category}" : null,
|
||||
])->filter()->implode(' and ');
|
||||
|
||||
$scopeLine = $scope
|
||||
? "Limit the review to assets in {$scope}."
|
||||
: 'Review assets across the entire organisation.';
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are conducting an end-of-life and depreciation review. {$scopeLine}
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. List assets that have passed their asset_eol_date (end-of-life date is in the past).
|
||||
2. List assets that are fully depreciated based on their depreciation schedule and purchase date.
|
||||
3. For each identified asset, show: asset tag, name, model, assigned user or location, EOL date, purchase date, and current status.
|
||||
4. Group findings by category for easier review.
|
||||
5. Recommend disposition for each group: retire and replace, redeploy to a lower-demand role, send for repair, or archive.
|
||||
6. Provide a cost summary if purchase cost data is available — total value of end-of-life assets.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('department', 'Limit review to a specific department', required: false),
|
||||
new Argument('category', 'Limit review to a specific asset category', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('expiring_licenses')]
|
||||
#[Title('Expiring Licenses')]
|
||||
#[Description('Review license seat usage and flag licenses expiring within a given number of days')]
|
||||
class ExpiringLicensesPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$days = (int) ($request->get('days', 30));
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are reviewing software license health across the organisation. Focus on licenses expiring within {$days} days.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. List all licenses in the system.
|
||||
2. Identify licenses whose expiration date falls within the next {$days} days.
|
||||
3. For each expiring license, show: license name, total seats, seats in use, seats free, and the expiration date.
|
||||
4. Flag any licenses that are over-deployed (more seats checked out than purchased).
|
||||
5. Flag any licenses that are under-used (many free seats that may indicate unused subscriptions worth cancelling).
|
||||
6. Produce a prioritised action list: renewals needed urgently, over-deployments to resolve, and possible cancellations.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('days', 'Number of days ahead to check for expiring licenses (default: 30)', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('find_available_asset')]
|
||||
#[Title('Find Available Asset')]
|
||||
#[Description('Find an undeployed asset by category or model and optionally check it out to a user')]
|
||||
class FindAvailableAssetPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$category = $request->get('category');
|
||||
$model = $request->get('model');
|
||||
$assignTo = $request->get('assign_to');
|
||||
|
||||
$assetDescription = collect([
|
||||
$category ? "category: {$category}" : null,
|
||||
$model ? "model: {$model}" : null,
|
||||
])->filter()->implode(' / ');
|
||||
|
||||
$assignLine = $assignTo
|
||||
? "If a suitable asset is found, check it out to the user: {$assignTo}."
|
||||
: 'Ask whether the found asset should be checked out to a specific user before proceeding.';
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You need to find an available (undeployed) asset matching {$assetDescription}.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. Search for assets with a Ready-to-Deploy status that match the requested {$assetDescription}.
|
||||
2. If multiple options are available, list them with their asset tags, serial numbers, and any relevant details so the best one can be selected.
|
||||
3. {$assignLine}
|
||||
4. Confirm the final asset tag, serial number, and checkout status once complete.
|
||||
|
||||
If no available assets match, report what was found and suggest alternatives (different models in the same category, or assets currently out for repair that may return soon).
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('category', 'Asset category to search (e.g. Laptop, Monitor)', required: false),
|
||||
new Argument('model', 'Specific model name to search for', required: false),
|
||||
new Argument('assign_to', 'Username to check the asset out to once found', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('inventory_summary')]
|
||||
#[Title('Inventory Summary')]
|
||||
#[Description('Produce a high-level inventory count by category, broken down by deployment status')]
|
||||
class InventorySummaryPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$location = $request->get('location');
|
||||
$department = $request->get('department');
|
||||
|
||||
$scope = collect([
|
||||
$location ? "location: {$location}" : null,
|
||||
$department ? "department: {$department}" : null,
|
||||
])->filter()->implode(' and ');
|
||||
|
||||
$scopeLine = $scope
|
||||
? "Scope the report to {$scope}."
|
||||
: 'Report across the entire organisation.';
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are generating an inventory summary report. {$scopeLine}
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. List assets (filtered by the scope above if provided) and tally counts by status: Deployed, Ready to Deploy, Archived, Pending, Out for Repair.
|
||||
2. Break the deployed count down by asset category (laptops, monitors, phones, etc.).
|
||||
3. List the top 5 models by total quantity.
|
||||
4. Show total purchase value of the inventory if cost data is available.
|
||||
5. Highlight any categories with zero available (Ready to Deploy) assets — potential stock-out risk.
|
||||
6. Present the results as a concise executive summary with a supporting breakdown table.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('location', 'Limit report to a specific location', required: false),
|
||||
new Argument('department', 'Limit report to a specific department', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('offboard_employee')]
|
||||
#[Title('Offboard Employee')]
|
||||
#[Description('Guide through checking in all equipment and licenses from a departing employee and deactivating their account')]
|
||||
class OffboardEmployeePrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$username = $request->get('username');
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are helping offboard a departing employee with username: {$username}.
|
||||
|
||||
Please complete the following offboarding steps using the available tools:
|
||||
|
||||
1. Look up the user account for {$username} and display a summary of everything currently assigned to them (assets, licenses, accessories, consumables).
|
||||
2. Check in all assigned assets from this user.
|
||||
3. Check in all assigned accessories from this user.
|
||||
4. Revoke or check in any license seats assigned to this user.
|
||||
5. Deactivate the user account.
|
||||
6. Provide a final summary of all items that were checked in and confirm the account has been deactivated.
|
||||
|
||||
If any items cannot be checked in automatically, flag them for manual follow-up.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('username', 'Username of the departing employee', required: true),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('onboard_employee')]
|
||||
#[Title('Onboard Employee')]
|
||||
#[Description('Guide through creating a new employee account and assigning appropriate equipment and licenses')]
|
||||
class OnboardEmployeePrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$firstName = $request->get('first_name');
|
||||
$lastName = $request->get('last_name');
|
||||
$department = $request->get('department');
|
||||
$location = $request->get('location');
|
||||
$title = $request->get('title');
|
||||
|
||||
$fullName = trim("{$firstName} {$lastName}");
|
||||
|
||||
$context = collect([
|
||||
$department ? "Department: {$department}" : null,
|
||||
$location ? "Location: {$location}" : null,
|
||||
$title ? "Job title: {$title}" : null,
|
||||
])->filter()->implode("\n");
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are helping onboard a new employee.
|
||||
|
||||
Employee details:
|
||||
- First name: {$firstName}
|
||||
- Last name: {$lastName}
|
||||
{$context}
|
||||
|
||||
Please complete the following onboarding steps using the available tools:
|
||||
|
||||
1. Create a new user account using first_name "{$firstName}" and last_name "{$lastName}" along with the details provided above. Ask for any missing required fields (username and, optionally, email address) before proceeding. Do not ask for a password — one will be set automatically.
|
||||
2. If the new account has an email address, ask whether you should send them a password reset link so they can set their own password. Use send_password_reset if the answer is yes.
|
||||
3. Search for available (undeployed) assets suitable for their role — typically a laptop and any other standard equipment for their department or location.
|
||||
4. Check out the selected assets to the new user.
|
||||
5. Check whether any software license seats are available that should be assigned (e.g. productivity suites, VPN, etc.) and assign them.
|
||||
6. Summarise what was set up: the user account created, whether a password reset email was sent, assets checked out, and licenses assigned.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('first_name', 'First name of the new employee', required: true),
|
||||
new Argument('last_name', 'Last name of the new employee', required: false),
|
||||
new Argument('department', 'Department the employee will join', required: false),
|
||||
new Argument('location', 'Primary office location', required: false),
|
||||
new Argument('title', 'Job title', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
|
||||
abstract class SnipePrompt extends Prompt
|
||||
{
|
||||
/**
|
||||
* Returns a trailing instruction telling the model which language to respond in,
|
||||
* derived from the authenticated user's locale setting. Returns an empty string
|
||||
* for English locales so the prompt text is unchanged for the majority of users.
|
||||
*/
|
||||
protected function localeInstruction(): string
|
||||
{
|
||||
$locale = auth()->user()?->locale ?? app()->getLocale();
|
||||
|
||||
if (str_starts_with($locale, 'en')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return "\n\nPlease respond in the language that corresponds to locale: {$locale}.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('user_inventory')]
|
||||
#[Title('User Inventory')]
|
||||
#[Description('List everything currently assigned to a specific user across all asset types')]
|
||||
class UserInventoryPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$username = $request->get('username');
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are pulling a complete inventory of everything assigned to the user: {$username}.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. Look up the user account for {$username} and display their basic info (name, department, location, job title).
|
||||
2. List all assets currently checked out to this user (asset tag, name, model, serial, status).
|
||||
3. List all accessories checked out to this user.
|
||||
4. List all license seats assigned to this user.
|
||||
5. List any consumables that have been checked out to this user.
|
||||
6. Calculate the total purchase value of all assigned assets if cost data is available.
|
||||
7. Present a clean summary grouped by item type, suitable for sharing with a manager or for an audit.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('username', 'Username of the user to review', required: true),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('warranty_expiring')]
|
||||
#[Title('Warranty Expiring')]
|
||||
#[Description('List assets whose warranty expires within a given number of days')]
|
||||
class WarrantyExpiringPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$days = (int) ($request->get('days', 90));
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are reviewing assets whose warranty is expiring soon. Focus on assets expiring within {$days} days.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. List assets and filter for those whose warranty expiration date (calculated from purchase_date + warranty_months) falls within the next {$days} days.
|
||||
2. For each asset, show: asset tag, name, model, assigned user or location, purchase date, warranty months, and calculated warranty end date.
|
||||
3. Group by urgency: expiring within 30 days, 31–60 days, and 61–{$days} days.
|
||||
4. Flag any assets that are deployed to critical roles or users where warranty coverage is especially important.
|
||||
5. Recommend actions: extend warranty, schedule replacement, or note as acceptable risk.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('days', 'Number of days ahead to check for warranty expiry (default: 90)', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
+1066
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Servers;
|
||||
|
||||
use App\Mcp\Prompts\AuditLocationPrompt;
|
||||
use App\Mcp\Prompts\EndOfLifeReviewPrompt;
|
||||
use App\Mcp\Prompts\ExpiringLicensesPrompt;
|
||||
use App\Mcp\Prompts\FindAvailableAssetPrompt;
|
||||
use App\Mcp\Prompts\InventorySummaryPrompt;
|
||||
use App\Mcp\Prompts\OffboardEmployeePrompt;
|
||||
use App\Mcp\Prompts\OnboardEmployeePrompt;
|
||||
use App\Mcp\Prompts\UserInventoryPrompt;
|
||||
use App\Mcp\Prompts\WarrantyExpiringPrompt;
|
||||
use App\Mcp\Tools\AddAssetNoteTool;
|
||||
use App\Mcp\Tools\AuditAssetTool;
|
||||
use App\Mcp\Tools\CheckinAccessoryTool;
|
||||
use App\Mcp\Tools\CheckinAssetTool;
|
||||
use App\Mcp\Tools\CheckinComponentTool;
|
||||
use App\Mcp\Tools\CheckinLicenseTool;
|
||||
use App\Mcp\Tools\CheckoutAccessoryTool;
|
||||
use App\Mcp\Tools\CheckoutAssetTool;
|
||||
use App\Mcp\Tools\CheckoutComponentTool;
|
||||
use App\Mcp\Tools\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\ListAssetNotesTool;
|
||||
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\ListHistoryTool;
|
||||
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\ListUploadsTool;
|
||||
use App\Mcp\Tools\ListUsersTool;
|
||||
use App\Mcp\Tools\Reset2FATool;
|
||||
use App\Mcp\Tools\RestoreAssetTool;
|
||||
use App\Mcp\Tools\RestoreUserTool;
|
||||
use App\Mcp\Tools\SendPasswordResetTool;
|
||||
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\UpdateProfileTool;
|
||||
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;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Version;
|
||||
|
||||
#[Name('Snipe-IT MCP Server')]
|
||||
#[Version('0.0.1')]
|
||||
#[Instructions('This server allows you to interact with the Snipe-IT asset management database. You can list, view, check out, and check in assets.')]
|
||||
class SnipeMCPServer extends Server
|
||||
{
|
||||
protected array $tools = [
|
||||
// Assets
|
||||
ShowAssetTool::class,
|
||||
ListAssetsTool::class,
|
||||
CreateAssetTool::class,
|
||||
UpdateAssetTool::class,
|
||||
DeleteAssetTool::class,
|
||||
RestoreAssetTool::class,
|
||||
CheckoutAssetTool::class,
|
||||
CheckinAssetTool::class,
|
||||
AuditAssetTool::class,
|
||||
AddAssetNoteTool::class,
|
||||
ListAssetNotesTool::class,
|
||||
|
||||
// Cross-type tools
|
||||
ListUploadsTool::class,
|
||||
ListHistoryTool::class,
|
||||
|
||||
// Users
|
||||
ListUsersTool::class,
|
||||
ShowUserTool::class,
|
||||
CreateUserTool::class,
|
||||
UpdateUserTool::class,
|
||||
DeleteUserTool::class,
|
||||
RestoreUserTool::class,
|
||||
GetCurrentUserTool::class,
|
||||
UpdateProfileTool::class,
|
||||
GetUserAssetsTool::class,
|
||||
Reset2FATool::class,
|
||||
SendPasswordResetTool::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,
|
||||
UpdateLicenseTool::class,
|
||||
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 = [
|
||||
//
|
||||
];
|
||||
|
||||
protected array $prompts = [
|
||||
OnboardEmployeePrompt::class,
|
||||
OffboardEmployeePrompt::class,
|
||||
AuditLocationPrompt::class,
|
||||
FindAvailableAssetPrompt::class,
|
||||
ExpiringLicensesPrompt::class,
|
||||
EndOfLifeReviewPrompt::class,
|
||||
WarrantyExpiringPrompt::class,
|
||||
InventorySummaryPrompt::class,
|
||||
UserInventoryPrompt::class,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('add_asset_note')]
|
||||
#[Title('Add Asset Note')]
|
||||
#[Description('Add a manual note to a Snipe-IT asset identified by asset tag, serial number, or numeric ID')]
|
||||
class AddAssetNoteTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|string|max:100',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'id' => 'nullable|integer',
|
||||
'note' => 'required|string|max:50000',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('update', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$logaction = new Actionlog;
|
||||
$logaction->item_type = Asset::class;
|
||||
$logaction->item_id = $asset->id;
|
||||
$logaction->note = $request->get('note');
|
||||
$logaction->created_by = auth()->id();
|
||||
|
||||
if ($logaction->logaction('note added')) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.note_added_to_asset', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.note_added_successfully'),
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'asset_id' => $asset->id,
|
||||
'note' => $logaction->note,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.note_save_failed')));
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))->first();
|
||||
}
|
||||
if ($request->filled('serial')) {
|
||||
return Asset::where('serial', $request->get('serial'))->first();
|
||||
}
|
||||
if ($request->filled('id')) {
|
||||
return Asset::find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset'),
|
||||
'serial' => $schema->string()->description('Serial number of the asset'),
|
||||
'id' => $schema->number()->description('Numeric ID of the asset'),
|
||||
'note' => $schema->string()->description('Note text to add to the asset'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the note was saved'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset'),
|
||||
'asset_id' => $schema->number()->description('Numeric ID of the asset'),
|
||||
'note' => $schema->string()->description('The note that was saved'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Setting;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('audit_asset')]
|
||||
#[Title('Audit Asset')]
|
||||
#[Description('Record an audit for a Snipe-IT asset, updating the last audit date and optionally the location')]
|
||||
class AuditAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'id' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:1000',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'next_audit_date' => 'nullable|date',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('audit', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$originalValues = $asset->getRawOriginal();
|
||||
$settings = Setting::getSettings();
|
||||
|
||||
$asset->last_audit_date = date('Y-m-d H:i:s');
|
||||
|
||||
if ($request->filled('next_audit_date')) {
|
||||
$asset->next_audit_date = $request->get('next_audit_date');
|
||||
} elseif (! is_null($settings->audit_interval)) {
|
||||
$asset->next_audit_date = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$asset->location_id = $request->get('location_id');
|
||||
}
|
||||
|
||||
// Bypass the observer to avoid logging a spurious asset-update entry
|
||||
// alongside the audit log entry created by logAudit() below
|
||||
$asset->unsetEventDispatcher();
|
||||
|
||||
if ($asset->isValid() && $asset->save()) {
|
||||
$asset->logAudit($request->get('note'), $request->get('location_id'), null, $originalValues);
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_audited', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_audited', ['asset_tag' => $asset->asset_tag]),
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'last_audit_date' => $asset->last_audit_date,
|
||||
'next_audit_date' => $asset->next_audit_date,
|
||||
'location' => $asset->location?->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.audit_failed', ['error' => $asset->getErrors()->first()])));
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))->with('location')->first();
|
||||
}
|
||||
if ($request->filled('serial')) {
|
||||
return Asset::where('serial', $request->get('serial'))->with('location')->first();
|
||||
}
|
||||
if ($request->filled('id')) {
|
||||
return Asset::with('location')->find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset to audit'),
|
||||
'serial' => $schema->string()->description('Serial number of the asset to audit'),
|
||||
'id' => $schema->number()->description('Numeric ID of the asset to audit'),
|
||||
'note' => $schema->string()->description('Optional audit note'),
|
||||
'location_id' => $schema->number()->description('Location ID where the asset was found (also updates the asset location)'),
|
||||
'next_audit_date' => $schema->string()->description('Override the next audit date (YYYY-MM-DD); defaults to now plus the audit_interval from settings'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the audit succeeded'),
|
||||
'error' => $schema->boolean()->description('True if the audit failed'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the audited asset'),
|
||||
'last_audit_date' => $schema->string()->description('Timestamp of the audit just recorded'),
|
||||
'next_audit_date' => $schema->string()->description('Date of the next scheduled audit'),
|
||||
'location' => $schema->string()->description('Location name where the asset was found'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\AccessoryCheckout;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkin_accessory')]
|
||||
#[Title('Checkin Accessory')]
|
||||
#[Description('Check in a Snipe-IT accessory checkout record by its checkout ID')]
|
||||
class CheckinAccessoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'checkout_id' => 'required|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$checkout = AccessoryCheckout::find($request->get('checkout_id'));
|
||||
|
||||
if (! $checkout) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_checkout_not_found')));
|
||||
}
|
||||
|
||||
$accessory = Accessory::find($checkout->accessory_id);
|
||||
|
||||
if (! $accessory) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkin', $accessory)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$target = $checkout->assigned_type && $checkout->assigned_to
|
||||
? $checkout->assigned_type::find($checkout->assigned_to)
|
||||
: null;
|
||||
|
||||
$accessory->logCheckin($target, $request->get('note'));
|
||||
|
||||
if ($checkout->delete()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.accessory_checked_in', ['name' => $accessory->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.accessory_checked_in', ['name' => $accessory->name]),
|
||||
'accessory_id' => $accessory->id,
|
||||
'accessory_name' => $accessory->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkin_failed')));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'checkout_id' => $schema->number()->description('ID of the checkout record to check in (returned by checkout_accessory)'),
|
||||
'note' => $schema->string()->description('Optional checkin note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkin succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'accessory_id' => $schema->number()->description('Numeric ID of the accessory'),
|
||||
'accessory_name' => $schema->string()->description('Name of the accessory'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkin_asset')]
|
||||
#[Title('Check In Asset')]
|
||||
#[Description('Check a currently checked-out Snipe-IT asset back in')]
|
||||
class CheckinAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'id' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:1000',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkin', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$target = $asset->assignedTo;
|
||||
|
||||
if (is_null($target)) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_checked_out', ['asset_tag' => $asset->asset_tag])));
|
||||
}
|
||||
|
||||
$originalValues = $asset->getRawOriginal();
|
||||
$checkinAt = date('Y-m-d H:i:s');
|
||||
|
||||
$asset->expected_checkin = null;
|
||||
$asset->last_checkin = now();
|
||||
$asset->assignedTo()->disassociate($asset);
|
||||
$asset->accepted = null;
|
||||
$asset->location_id = $asset->rtd_location_id;
|
||||
|
||||
if ($asset->save()) {
|
||||
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->get('note'), $checkinAt, $originalValues));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_checked_in', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_checked_in', ['asset_tag' => $asset->asset_tag]),
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'model' => $asset->model?->name,
|
||||
'location' => $asset->location?->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkin_failed_error', ['error' => $asset->getErrors()->first()])));
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))
|
||||
->with('model', 'location')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($request->filled('id')) {
|
||||
return Asset::with('model', 'location')->find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()
|
||||
->description('Asset tag of the asset to check in'),
|
||||
'id' => $schema->number()
|
||||
->description('Numeric ID of the asset to check in'),
|
||||
'note' => $schema->string()
|
||||
->description('Optional note to attach to this checkin'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->string()->description('True if the checkin succeeded'),
|
||||
'error' => $schema->string()->description('True if the checkin failed'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the checked-in asset'),
|
||||
'model' => $schema->string()->description('Model name of the checked-in asset'),
|
||||
'location' => $schema->string()->description('Location the asset returned to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Component;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkin_component')]
|
||||
#[Title('Checkin Component')]
|
||||
#[Description('Check in one or more units of a Snipe-IT component from an asset using the checkout record ID')]
|
||||
class CheckinComponentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'component_asset_id' => 'required|integer',
|
||||
'checkin_qty' => 'nullable|integer|min:1',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$componentAsset = DB::table('components_assets')->find($request->get('component_asset_id'));
|
||||
|
||||
if (! $componentAsset) {
|
||||
return Response::make(Response::error(trans('mcp.component_checkout_not_found')));
|
||||
}
|
||||
|
||||
$component = Component::find($componentAsset->component_id);
|
||||
|
||||
if (! $component) {
|
||||
return Response::make(Response::error(trans('mcp.component_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkin', $component)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$maxCheckin = $componentAsset->assigned_qty ?? 1;
|
||||
$checkinQty = (int) $request->get('checkin_qty', $maxCheckin);
|
||||
|
||||
if ($checkinQty > $maxCheckin) {
|
||||
return Response::make(Response::error(
|
||||
'Checkin quantity ('.$checkinQty.') exceeds assigned quantity ('.$maxCheckin.')'
|
||||
));
|
||||
}
|
||||
|
||||
$remaining = $maxCheckin - $checkinQty;
|
||||
|
||||
if ($remaining === 0) {
|
||||
DB::table('components_assets')->where('id', $componentAsset->id)->delete();
|
||||
} else {
|
||||
DB::table('components_assets')->where('id', $componentAsset->id)->update(['assigned_qty' => $remaining]);
|
||||
}
|
||||
|
||||
$asset = Asset::find($componentAsset->asset_id);
|
||||
|
||||
event(new CheckoutableCheckedIn($component, $asset, auth()->user(), $request->get('note'), Carbon::now()));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.component_checked_in', ['name' => $component->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.component_checked_in', ['name' => $component->name]),
|
||||
'component_id' => $component->id,
|
||||
'component_name' => $component->name,
|
||||
'checkin_qty' => $checkinQty,
|
||||
'qty_still_checked_out' => $remaining,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'component_asset_id' => $schema->number()->description('ID of the checkout record to check in (returned by checkout_component)'),
|
||||
'checkin_qty' => $schema->number()->description('Number of units to check in (default: all assigned units)'),
|
||||
'note' => $schema->string()->description('Optional checkin note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkin succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'component_id' => $schema->number()->description('Numeric ID of the component'),
|
||||
'component_name' => $schema->string()->description('Name of the component'),
|
||||
'checkin_qty' => $schema->number()->description('Number of units checked in'),
|
||||
'qty_still_checked_out' => $schema->number()->description('Units remaining checked out on this record (0 means fully returned)'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkin_license')]
|
||||
#[Title('Checkin License')]
|
||||
#[Description('Check in a Snipe-IT license seat by its seat ID, returning it to the available pool')]
|
||||
class CheckinLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'seat_id' => 'required|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$seat = LicenseSeat::with('license')->find($request->get('seat_id'));
|
||||
|
||||
if (! $seat) {
|
||||
return Response::make(Response::error(trans('mcp.license_seat_not_found')));
|
||||
}
|
||||
|
||||
if (is_null($seat->assigned_to) && is_null($seat->asset_id)) {
|
||||
return Response::make(Response::error(trans('mcp.seat_not_checked_out')));
|
||||
}
|
||||
|
||||
$license = $seat->license;
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error(trans('mcp.license_not_found')));
|
||||
}
|
||||
|
||||
// License checkin uses the checkout gate (matching application behavior)
|
||||
if (! Gate::allows('checkout', $license)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$returnTo = null;
|
||||
if ($seat->assigned_to) {
|
||||
$returnTo = User::withTrashed()->find($seat->assigned_to);
|
||||
} elseif ($seat->asset_id) {
|
||||
$returnTo = Asset::find($seat->asset_id);
|
||||
}
|
||||
|
||||
$note = $request->get('note');
|
||||
|
||||
$seat->assigned_to = null;
|
||||
$seat->asset_id = null;
|
||||
$seat->notes = $note;
|
||||
|
||||
if (! $license->reassignable) {
|
||||
$seat->unreassignable_seat = true;
|
||||
}
|
||||
|
||||
if ($seat->save()) {
|
||||
event(new CheckoutableCheckedIn($seat, $returnTo, auth()->user(), $note));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_seat_checked_in', ['id' => $seat->id]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_seat_checked_in', ['id' => $seat->id]),
|
||||
'seat_id' => $seat->id,
|
||||
'license_id' => $license->id,
|
||||
'license_name' => $license->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkin_failed')));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'seat_id' => $schema->number()->description('ID of the license seat to check in (returned by checkout_license)'),
|
||||
'note' => $schema->string()->description('Optional checkin note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkin succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'seat_id' => $schema->number()->description('ID of the seat that was checked in'),
|
||||
'license_id' => $schema->number()->description('Numeric ID of the license'),
|
||||
'license_name' => $schema->string()->description('Name of the license'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\AccessoryCheckout;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Location;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_accessory')]
|
||||
#[Title('Checkout Accessory')]
|
||||
#[Description('Check out a Snipe-IT accessory to a user, location, or asset')]
|
||||
class CheckoutAccessoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'checkout_to_type' => 'required|in:user,location,asset',
|
||||
'assigned_user' => 'nullable|integer',
|
||||
'assigned_location' => 'nullable|integer',
|
||||
'assigned_asset' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$accessory = $this->resolveAccessory($request);
|
||||
|
||||
if (! $accessory) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $accessory)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($accessory->numRemaining() < 1) {
|
||||
return Response::make(Response::error(trans('mcp.no_units_available')));
|
||||
}
|
||||
|
||||
$checkoutType = $request->get('checkout_to_type');
|
||||
|
||||
$target = match ($checkoutType) {
|
||||
'user' => User::find($request->get('assigned_user')),
|
||||
'location' => Location::find($request->get('assigned_location')),
|
||||
'asset' => Asset::find($request->get('assigned_asset')),
|
||||
};
|
||||
|
||||
if (! $target) {
|
||||
return Response::make(Response::error(trans('mcp.checkout_target_not_found', ['type' => $checkoutType])));
|
||||
}
|
||||
|
||||
$checkout = new AccessoryCheckout([
|
||||
'accessory_id' => $accessory->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_to' => $target->id,
|
||||
'assigned_type' => $target::class,
|
||||
'note' => $request->get('note'),
|
||||
]);
|
||||
$checkout->created_by = auth()->id();
|
||||
$checkout->save();
|
||||
|
||||
event(new CheckoutableCheckedOut(
|
||||
$accessory,
|
||||
$target,
|
||||
auth()->user(),
|
||||
$request->get('note'),
|
||||
[],
|
||||
1,
|
||||
));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.accessory_checked_out', ['name' => $accessory->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.accessory_checked_out', ['name' => $accessory->name]),
|
||||
'accessory_id' => $accessory->id,
|
||||
'accessory_name' => $accessory->name,
|
||||
'checkout_id' => $checkout->id,
|
||||
'checked_out_to_type' => $checkoutType,
|
||||
'checked_out_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAccessory(Request $request): ?Accessory
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Accessory::withCount('checkouts as checkouts_count')->find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Accessory::withCount('checkouts as checkouts_count')->where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the accessory to check out'),
|
||||
'name' => $schema->string()->description('Name of the accessory to check out'),
|
||||
'checkout_to_type' => $schema->string()->description('Target type: user, location, or asset (required)'),
|
||||
'assigned_user' => $schema->number()->description('User ID to check out to'),
|
||||
'assigned_location' => $schema->number()->description('Location ID to check out to'),
|
||||
'assigned_asset' => $schema->number()->description('Asset ID to check out to'),
|
||||
'note' => $schema->string()->description('Optional checkout note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkout succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'accessory_id' => $schema->number()->description('Numeric ID of the accessory'),
|
||||
'accessory_name' => $schema->string()->description('Name of the accessory'),
|
||||
'checkout_id' => $schema->number()->description('ID of the checkout record (use this for checkin)'),
|
||||
'checked_out_to_type' => $schema->string()->description('Type of target: user, location, or asset'),
|
||||
'checked_out_to_id' => $schema->number()->description('ID of the target'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Location;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_asset')]
|
||||
#[Title('Checkout Asset')]
|
||||
#[Description('Check out a Snipe-IT asset to a user, location, or another asset')]
|
||||
class CheckoutAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'id' => 'nullable|integer',
|
||||
'checkout_to_type' => 'required|string|in:user,location,asset',
|
||||
'assigned_user' => 'nullable|integer',
|
||||
'assigned_location' => 'nullable|integer',
|
||||
'assigned_asset' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:1000',
|
||||
'checkout_at' => 'nullable|date',
|
||||
'expected_checkin' => 'nullable|date',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if (! $asset->availableForCheckout()) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_available', ['asset_tag' => $asset->asset_tag])));
|
||||
}
|
||||
|
||||
$checkoutType = $request->get('checkout_to_type');
|
||||
$target = null;
|
||||
|
||||
if ($checkoutType === 'user') {
|
||||
$target = User::find($request->get('assigned_user'));
|
||||
if ($target) {
|
||||
$asset->location_id = $target->location_id ?? $asset->location_id;
|
||||
}
|
||||
} elseif ($checkoutType === 'location') {
|
||||
$target = Location::find($request->get('assigned_location'));
|
||||
if ($target) {
|
||||
$asset->location_id = $target->id;
|
||||
}
|
||||
} elseif ($checkoutType === 'asset') {
|
||||
$target = Asset::where('id', '!=', $asset->id)->find($request->get('assigned_asset'));
|
||||
if ($target) {
|
||||
$asset->location_id = $target->location_id ?? $asset->location_id;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $target) {
|
||||
return Response::make(Response::error(trans('mcp.checkout_target_not_found', ['type' => $checkoutType])));
|
||||
}
|
||||
|
||||
$checkoutAt = $request->filled('checkout_at') ? $request->get('checkout_at') : date('Y-m-d H:i:s');
|
||||
$expectedCheckin = $request->filled('expected_checkin') ? $request->get('expected_checkin') : null;
|
||||
$note = $request->filled('note') ? $request->get('note') : null;
|
||||
|
||||
if ($asset->checkOut($target, auth()->user(), $checkoutAt, $expectedCheckin, $note, $asset->name, $asset->location_id)) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_checked_out', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_checked_out', ['asset_tag' => $asset->asset_tag]),
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'checked_out_to_type' => $checkoutType,
|
||||
'checked_out_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkout_failed')));
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))
|
||||
->with('status')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($request->filled('id')) {
|
||||
return Asset::with('status')->find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()
|
||||
->description('Asset tag of the asset to check out'),
|
||||
'id' => $schema->number()
|
||||
->description('Numeric ID of the asset to check out'),
|
||||
'checkout_to_type' => $schema->string()
|
||||
->description('What to check the asset out to: user, location, or asset')
|
||||
->required(),
|
||||
'assigned_user' => $schema->number()
|
||||
->description('ID of the user to check the asset out to (when checkout_to_type is user)'),
|
||||
'assigned_location' => $schema->number()
|
||||
->description('ID of the location to check the asset out to (when checkout_to_type is location)'),
|
||||
'assigned_asset' => $schema->number()
|
||||
->description('ID of the asset to check the asset out to (when checkout_to_type is asset)'),
|
||||
'note' => $schema->string()
|
||||
->description('Optional note to attach to this checkout'),
|
||||
'checkout_at' => $schema->string()
|
||||
->description('Checkout date/time (defaults to now, format: YYYY-MM-DD)'),
|
||||
'expected_checkin' => $schema->string()
|
||||
->description('Expected checkin date (format: YYYY-MM-DD)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->string()->description('True if the checkout succeeded'),
|
||||
'error' => $schema->string()->description('True if the checkout failed'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the checked-out asset'),
|
||||
'checked_out_to_type' => $schema->string()->description('Type of entity the asset was checked out to'),
|
||||
'checked_out_to_id' => $schema->number()->description('ID of the entity the asset was checked out to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Component;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_component')]
|
||||
#[Title('Checkout Component')]
|
||||
#[Description('Check out one or more units of a Snipe-IT component to an asset')]
|
||||
class CheckoutComponentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:191',
|
||||
'asset_id' => 'required|integer|exists:assets,id',
|
||||
'assigned_qty' => 'nullable|integer|min:1',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$component = $this->resolveComponent($request);
|
||||
|
||||
if (! $component) {
|
||||
return Response::make(Response::error(trans('mcp.component_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $component)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$qty = (int) $request->get('assigned_qty', 1);
|
||||
|
||||
if ($component->numRemaining() < $qty) {
|
||||
return Response::make(Response::error(
|
||||
'Not enough units available. Requested: '.$qty.', remaining: '.$component->numRemaining()
|
||||
));
|
||||
}
|
||||
|
||||
$asset = Asset::find($request->get('asset_id'));
|
||||
|
||||
$component->assets()->attach($component->id, [
|
||||
'component_id' => $component->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_qty' => $qty,
|
||||
'created_by' => auth()->id(),
|
||||
'asset_id' => $asset->id,
|
||||
'note' => $request->get('note'),
|
||||
]);
|
||||
|
||||
$pivotId = $component->assets()->wherePivot('asset_id', $asset->id)->latest('components_assets.created_at')->first()?->pivot->id;
|
||||
|
||||
$component->logCheckout($request->get('note'), $asset, null, [], $qty);
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.component_checked_out', ['name' => $component->name, 'asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.component_checked_out', ['name' => $component->name, 'asset_tag' => $asset->asset_tag]),
|
||||
'component_id' => $component->id,
|
||||
'component_name' => $component->name,
|
||||
'asset_id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'assigned_qty' => $qty,
|
||||
'component_asset_id' => $pivotId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveComponent(Request $request): ?Component
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Component::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Component::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the component to check out'),
|
||||
'name' => $schema->string()->description('Name of the component to check out'),
|
||||
'asset_id' => $schema->number()->description('Asset ID to check the component out to (required)'),
|
||||
'assigned_qty' => $schema->number()->description('Number of units to check out (default: 1)'),
|
||||
'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(),
|
||||
'component_id' => $schema->number()->description('Numeric ID of the component'),
|
||||
'component_name' => $schema->string()->description('Name of the component'),
|
||||
'asset_id' => $schema->number()->description('ID of the asset checked out to'),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset checked out to'),
|
||||
'assigned_qty' => $schema->number()->description('Number of units checked out'),
|
||||
'component_asset_id' => $schema->number()->description('ID of the checkout record (use this for checkin)'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_consumable')]
|
||||
#[Title('Checkout Consumable')]
|
||||
#[Description('Check out a Snipe-IT consumable to a user')]
|
||||
class CheckoutConsumableTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->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(trans('mcp.consumable_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $consumable)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($consumable->numRemaining() <= 0) {
|
||||
return Response::make(Response::error(trans('mcp.no_units_remaining')));
|
||||
}
|
||||
|
||||
$user = User::find($request->get('assigned_to'));
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.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(trans('mcp.consumable_checked_out', ['name' => $consumable->name, 'username' => $user->username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.consumable_checked_out', ['name' => $consumable->name, 'username' => $user->username]),
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Models\Asset;
|
||||
use App\Models\License;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_license')]
|
||||
#[Title('Checkout License')]
|
||||
#[Description('Check out an available license seat to a user or asset')]
|
||||
class CheckoutLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'assigned_to' => 'nullable|integer',
|
||||
'asset_id' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$license = $this->resolveLicense($request);
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error(trans('mcp.license_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $license)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($license->numRemaining() < 1) {
|
||||
return Response::make(Response::error(trans('mcp.no_available_seats')));
|
||||
}
|
||||
|
||||
if (! $request->filled('assigned_to') && ! $request->filled('asset_id')) {
|
||||
return Response::make(Response::error(trans('mcp.provide_user_or_asset')));
|
||||
}
|
||||
|
||||
$seat = $license->freeSeat();
|
||||
|
||||
if (! $seat) {
|
||||
return Response::make(Response::error(trans('mcp.no_free_seat')));
|
||||
}
|
||||
|
||||
$note = $request->get('note');
|
||||
|
||||
if ($request->filled('assigned_to')) {
|
||||
$target = User::find($request->get('assigned_to'));
|
||||
if (! $target) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
$seat->assigned_to = $target->id;
|
||||
$seat->notes = $note;
|
||||
|
||||
if ($seat->save()) {
|
||||
event(new CheckoutableCheckedOut($seat, $target, auth()->user(), $note, [], 1));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_seat_checked_out_user', ['username' => $target->username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_seat_checked_out_user', ['username' => $target->username]),
|
||||
'license_id' => $license->id,
|
||||
'license_name' => $license->name,
|
||||
'seat_id' => $seat->id,
|
||||
'assigned_to_type' => 'user',
|
||||
'assigned_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
} elseif ($request->filled('asset_id')) {
|
||||
$target = Asset::find($request->get('asset_id'));
|
||||
if (! $target) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
$seat->asset_id = $target->id;
|
||||
if ($target->checkedOutToUser()) {
|
||||
$seat->assigned_to = $target->assigned_to;
|
||||
}
|
||||
$seat->notes = $note;
|
||||
|
||||
if ($seat->save()) {
|
||||
event(new CheckoutableCheckedOut($seat, $target, auth()->user(), $note, [], 1));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_seat_checked_out_asset', ['asset_tag' => $target->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_seat_checked_out_asset', ['asset_tag' => $target->asset_tag]),
|
||||
'license_id' => $license->id,
|
||||
'license_name' => $license->name,
|
||||
'seat_id' => $seat->id,
|
||||
'assigned_to_type' => 'asset',
|
||||
'assigned_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkout_failed')));
|
||||
}
|
||||
|
||||
private function resolveLicense(Request $request): ?License
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return License::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return License::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the license to check out'),
|
||||
'name' => $schema->string()->description('Name of the license to check out'),
|
||||
'assigned_to' => $schema->number()->description('User ID to assign the seat to'),
|
||||
'asset_id' => $schema->number()->description('Asset ID to assign the seat to'),
|
||||
'note' => $schema->string()->description('Optional checkout note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkout succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'license_id' => $schema->number()->description('Numeric ID of the license'),
|
||||
'license_name' => $schema->string()->description('Name of the license'),
|
||||
'seat_id' => $schema->number()->description('ID of the seat record (use this for checkin)'),
|
||||
'assigned_to_type' => $schema->string()->description('Type of entity checked out to: user or asset'),
|
||||
'assigned_to_id' => $schema->number()->description('ID of the entity checked out to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Company;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_accessory')]
|
||||
#[Title('Create Accessory')]
|
||||
#[Description('Create a new Snipe-IT accessory')]
|
||||
class CreateAccessoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Accessory::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'qty' => 'nullable|integer|min:0',
|
||||
'model_number' => 'nullable|string|max:255',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'order_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()));
|
||||
}
|
||||
|
||||
$accessory = new Accessory;
|
||||
$accessory->fill($request->only([
|
||||
'name', 'category_id', 'qty', 'model_number', 'manufacturer_id',
|
||||
'supplier_id', 'location_id', 'order_number', 'purchase_cost',
|
||||
'purchase_date', 'min_amt', 'requestable', 'notes',
|
||||
]));
|
||||
|
||||
$accessory->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$accessory->created_by = auth()->id();
|
||||
|
||||
if ($accessory->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.accessory_created', ['name' => $accessory->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.accessory_created', ['name' => $accessory->name]),
|
||||
'id' => $accessory->id,
|
||||
'name' => $accessory->name,
|
||||
'qty' => $accessory->qty,
|
||||
'category_id' => $accessory->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $accessory->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Accessory name (required)'),
|
||||
'category_id' => $schema->number()->description('Category ID — must be an accessory category (required)'),
|
||||
'qty' => $schema->number()->description('Total quantity in stock'),
|
||||
'model_number' => $schema->string()->description('Model number'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'order_number' => $schema->string()->description('Order 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 accessory'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the accessory was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new accessory'),
|
||||
'name' => $schema->string()->description('Name of the new accessory'),
|
||||
'qty' => $schema->number()->description('Total quantity'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\AssetModel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_asset_model')]
|
||||
#[Title('Create Asset Model')]
|
||||
#[Description('Create a new Snipe-IT asset model')]
|
||||
class CreateAssetModelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', AssetModel::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->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(trans('mcp.asset_model_created', ['name' => $assetModel->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_model_created', ['name' => $assetModel->name]),
|
||||
'id' => $assetModel->id,
|
||||
'name' => $assetModel->name,
|
||||
'category_id' => $assetModel->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_asset')]
|
||||
#[Title('Create Asset')]
|
||||
#[Description('Create a new Snipe-IT asset')]
|
||||
class CreateAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->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(trans('mcp.asset_created', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_created', ['asset_tag' => $asset->asset_tag]),
|
||||
'id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'name' => $asset->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_category')]
|
||||
#[Title('Create Category')]
|
||||
#[Description('Create a new Snipe-IT category')]
|
||||
class CreateCategoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Category::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->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(trans('mcp.category_created', ['name' => $category->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.category_created', ['name' => $category->name]),
|
||||
'id' => $category->id,
|
||||
'name' => $category->name,
|
||||
'category_type' => $category->category_type,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_company')]
|
||||
#[Title('Create Company')]
|
||||
#[Description('Create a new Snipe-IT company')]
|
||||
class CreateCompanyTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Company::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->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(trans('mcp.company_created', ['name' => $company->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.company_created', ['name' => $company->name]),
|
||||
'id' => $company->id,
|
||||
'name' => $company->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Component;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_component')]
|
||||
#[Title('Create Component')]
|
||||
#[Description('Create a new Snipe-IT component')]
|
||||
class CreateComponentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Component::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:191',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'qty' => 'required|integer|min:1',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'model_number' => 'nullable|string|max:255',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'order_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',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$component = new Component;
|
||||
$component->fill($request->only([
|
||||
'name', 'category_id', 'qty', 'serial', 'model_number',
|
||||
'manufacturer_id', 'supplier_id', 'location_id',
|
||||
'order_number', 'purchase_cost', 'purchase_date', 'min_amt', 'notes',
|
||||
]));
|
||||
|
||||
$component->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$component->created_by = auth()->id();
|
||||
|
||||
if ($component->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.component_created', ['name' => $component->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.component_created', ['name' => $component->name]),
|
||||
'id' => $component->id,
|
||||
'name' => $component->name,
|
||||
'qty' => $component->qty,
|
||||
'category_id' => $component->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $component->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Component name (required)'),
|
||||
'category_id' => $schema->number()->description('Category ID — must be a component category (required)'),
|
||||
'qty' => $schema->number()->description('Total quantity in stock (required, min 1)'),
|
||||
'serial' => $schema->string()->description('Serial number'),
|
||||
'model_number' => $schema->string()->description('Model number'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'order_number' => $schema->string()->description('Order 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'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the component was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new component'),
|
||||
'name' => $schema->string()->description('Name of the new component'),
|
||||
'qty' => $schema->number()->description('Total quantity'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Consumable;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_consumable')]
|
||||
#[Title('Create Consumable')]
|
||||
#[Description('Create a new Snipe-IT consumable')]
|
||||
class CreateConsumableTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Consumable::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->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(trans('mcp.consumable_created', ['name' => $consumable->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.consumable_created', ['name' => $consumable->name]),
|
||||
'id' => $consumable->id,
|
||||
'name' => $consumable->name,
|
||||
'qty' => $consumable->qty,
|
||||
'category_id' => $consumable->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Department;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_department')]
|
||||
#[Title('Create Department')]
|
||||
#[Description('Create a new Snipe-IT department')]
|
||||
class CreateDepartmentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Department::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'manager_id' => 'nullable|integer|exists:users,id',
|
||||
'phone' => 'nullable|string|max:255',
|
||||
'fax' => 'nullable|string|max:255',
|
||||
'notes' => 'nullable|string|max:255',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$department = new Department;
|
||||
$department->fill($request->only([
|
||||
'name', 'location_id', 'manager_id', 'phone', 'fax', 'notes',
|
||||
]));
|
||||
|
||||
$department->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$department->created_by = auth()->id();
|
||||
|
||||
if ($department->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.department_created', ['name' => $department->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.department_created', ['name' => $department->name]),
|
||||
'id' => $department->id,
|
||||
'name' => $department->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $department->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Department name (required)'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'manager_id' => $schema->number()->description('User ID of the department manager'),
|
||||
'phone' => $schema->string()->description('Department phone number'),
|
||||
'fax' => $schema->string()->description('Department fax number'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the department was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new department'),
|
||||
'name' => $schema->string()->description('Name of the new department'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Depreciation;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_depreciation')]
|
||||
#[Title('Create Depreciation')]
|
||||
#[Description('Create a new Snipe-IT depreciation schedule')]
|
||||
class CreateDepreciationTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Depreciation::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->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(trans('mcp.depreciation_created', ['name' => $depreciation->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.depreciation_created', ['name' => $depreciation->name]),
|
||||
'id' => $depreciation->id,
|
||||
'name' => $depreciation->name,
|
||||
'months' => $depreciation->months,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_group')]
|
||||
#[Title('Create Group')]
|
||||
#[Description('Create a new Snipe-IT permission group. Requires superadmin. Permissions are a JSON object mapping permission keys to 1 (grant) or -1 (deny).')]
|
||||
class CreateGroupTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('superadmin')) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'permissions' => 'nullable|string',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$permissions = null;
|
||||
if ($request->filled('permissions')) {
|
||||
$result = $this->parseAndValidatePermissions($request->get('permissions'));
|
||||
if (is_string($result)) {
|
||||
return Response::make(Response::error($result));
|
||||
}
|
||||
$permissions = $result;
|
||||
}
|
||||
|
||||
$group = new Group;
|
||||
$group->name = $request->get('name');
|
||||
if ($permissions !== null) {
|
||||
$group->permissions = json_encode($permissions);
|
||||
}
|
||||
if ($request->filled('notes')) {
|
||||
$group->notes = $request->get('notes');
|
||||
}
|
||||
$group->created_by = auth()->id();
|
||||
|
||||
if ($group->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.group_created', ['name' => $group->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.group_created', ['name' => $group->name]),
|
||||
'id' => $group->id,
|
||||
'name' => $group->name,
|
||||
'permissions' => $group->decodePermissions(),
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $group->getErrors()->first()])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a JSON permissions string and validate all keys against config('permissions').
|
||||
* Returns the decoded array on success, or an error string on failure.
|
||||
*/
|
||||
private function parseAndValidatePermissions(string $raw): array|string
|
||||
{
|
||||
$decoded = json_decode($raw, true);
|
||||
if (! is_array($decoded)) {
|
||||
return trans('mcp.invalid_permissions_format');
|
||||
}
|
||||
|
||||
$validKeys = collect(config('permissions'))
|
||||
->flatMap(fn ($perms) => collect($perms)->pluck('permission'))
|
||||
->unique()
|
||||
->flip()
|
||||
->all();
|
||||
|
||||
foreach (array_keys($decoded) as $key) {
|
||||
if (! isset($validKeys[$key])) {
|
||||
return trans('mcp.invalid_permission_key', ['key' => $key]);
|
||||
}
|
||||
if (! in_array((int) $decoded[$key], [1, -1], true)) {
|
||||
return trans('mcp.invalid_permission_value', ['key' => $key]);
|
||||
}
|
||||
}
|
||||
|
||||
return array_map('intval', $decoded);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Group name (required, must be unique)'),
|
||||
'permissions' => $schema->string()->description(
|
||||
'JSON object mapping permission keys to 1 (grant) or -1 (deny). '.
|
||||
'Valid keys include: superuser, admin, import, reports.view, '.
|
||||
'assets.view, assets.create, assets.edit, assets.delete, assets.checkout, assets.checkin, assets.audit, '.
|
||||
'users.view, users.create, users.edit, users.delete, '.
|
||||
'licenses.view, licenses.create, licenses.edit, licenses.delete, licenses.checkout, licenses.checkin, '.
|
||||
'accessories.view, accessories.create, accessories.edit, accessories.delete, accessories.checkout, accessories.checkin, '.
|
||||
'components.view, components.create, components.edit, components.delete, components.checkout, components.checkin, '.
|
||||
'consumables.view, consumables.create, consumables.edit, consumables.delete, consumables.checkout, '.
|
||||
'and many more. Example: {"assets.view":1,"assets.create":1,"assets.edit":-1}'
|
||||
),
|
||||
'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'),
|
||||
'permissions' => $schema->object()->description('Permissions set on the group'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\License;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_license')]
|
||||
#[Title('Create License')]
|
||||
#[Description('Create a new Snipe-IT software license')]
|
||||
class CreateLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', License::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'seats' => 'required|integer|min:1',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'purchase_date' => 'nullable|date_format:Y-m-d',
|
||||
'purchase_cost' => 'nullable|numeric|min:0',
|
||||
'purchase_order' => 'nullable|string|max:255',
|
||||
'order_number' => 'nullable|string|max:255',
|
||||
'expiration_date' => 'nullable|date_format:Y-m-d',
|
||||
'termination_date' => 'nullable|date_format:Y-m-d',
|
||||
'license_name' => 'nullable|string|max:255',
|
||||
'license_email' => 'nullable|email|max:255',
|
||||
'maintained' => 'nullable|boolean',
|
||||
'reassignable' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
'min_amt' => 'nullable|integer|min:0',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$license = new License;
|
||||
$license->fill($request->only([
|
||||
'name', 'seats', 'category_id', 'serial', 'manufacturer_id',
|
||||
'supplier_id', 'purchase_date', 'purchase_cost', 'purchase_order',
|
||||
'order_number', 'expiration_date', 'termination_date',
|
||||
'license_name', 'license_email', 'maintained', 'reassignable',
|
||||
'notes', 'min_amt',
|
||||
]));
|
||||
|
||||
$license->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$license->created_by = auth()->id();
|
||||
|
||||
if ($license->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_created', ['name' => $license->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_created', ['name' => $license->name]),
|
||||
'id' => $license->id,
|
||||
'name' => $license->name,
|
||||
'seats' => $license->seats,
|
||||
'category_id' => $license->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $license->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('License name (required)'),
|
||||
'seats' => $schema->number()->description('Number of seats (required, min 1)'),
|
||||
'category_id' => $schema->number()->description('Category ID — must be a license category (required)'),
|
||||
'serial' => $schema->string()->description('Product key / serial number'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'purchase_cost' => $schema->number()->description('Purchase cost'),
|
||||
'purchase_order' => $schema->string()->description('Purchase order number'),
|
||||
'order_number' => $schema->string()->description('Order number'),
|
||||
'expiration_date' => $schema->string()->description('License expiration date (YYYY-MM-DD)'),
|
||||
'termination_date' => $schema->string()->description('License termination date (YYYY-MM-DD)'),
|
||||
'license_name' => $schema->string()->description('Name of the licensed user/organization'),
|
||||
'license_email' => $schema->string()->description('Email of the licensed user/organization'),
|
||||
'maintained' => $schema->boolean()->description('Whether the license is under maintenance'),
|
||||
'reassignable' => $schema->boolean()->description('Whether seats can be reassigned after checkin'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'min_amt' => $schema->number()->description('Minimum seat threshold for alerts'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the license was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new license'),
|
||||
'name' => $schema->string()->description('Name of the new license'),
|
||||
'seats' => $schema->number()->description('Total seat count'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Location;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_location')]
|
||||
#[Title('Create Location')]
|
||||
#[Description('Create a new Snipe-IT location')]
|
||||
class CreateLocationTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Location::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->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(trans('mcp.location_created', ['name' => $location->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.location_created', ['name' => $location->name]),
|
||||
'id' => $location->id,
|
||||
'name' => $location->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Maintenance;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_maintenance')]
|
||||
#[Title('Create Maintenance')]
|
||||
#[Description('Create a new asset maintenance record')]
|
||||
class CreateMaintenanceTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('update', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->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(trans('mcp.maintenance_created', ['name' => $maintenance->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.maintenance_created', ['name' => $maintenance->name]),
|
||||
'id' => $maintenance->id,
|
||||
'title' => $maintenance->name,
|
||||
'asset_id' => $maintenance->asset_id,
|
||||
'asset_tag' => $maintenance->asset?->asset_tag,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Manufacturer;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_manufacturer')]
|
||||
#[Title('Create Manufacturer')]
|
||||
#[Description('Create a new Snipe-IT manufacturer')]
|
||||
class CreateManufacturerTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Manufacturer::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->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(trans('mcp.manufacturer_created', ['name' => $manufacturer->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.manufacturer_created', ['name' => $manufacturer->name]),
|
||||
'id' => $manufacturer->id,
|
||||
'name' => $manufacturer->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Statuslabel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_status_label')]
|
||||
#[Title('Create Status Label')]
|
||||
#[Description('Create a new Snipe-IT status label')]
|
||||
class CreateStatusLabelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Statuslabel::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->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(trans('mcp.status_label_created', ['name' => $statuslabel->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.status_label_created', ['name' => $statuslabel->name]),
|
||||
'id' => $statuslabel->id,
|
||||
'name' => $statuslabel->name,
|
||||
'type' => $statuslabel->getStatuslabelType(),
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Supplier;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_supplier')]
|
||||
#[Title('Create Supplier')]
|
||||
#[Description('Create a new Snipe-IT supplier')]
|
||||
class CreateSupplierTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Supplier::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->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(trans('mcp.supplier_created', ['name' => $supplier->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.supplier_created', ['name' => $supplier->name]),
|
||||
'id' => $supplier->id,
|
||||
'name' => $supplier->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Group;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_user')]
|
||||
#[Title('Create User')]
|
||||
#[Description('Create a new Snipe-IT user account')]
|
||||
class CreateUserTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', User::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'first_name' => 'required|string|max:191',
|
||||
'last_name' => 'nullable|string|max:191',
|
||||
'username' => 'required|string|max:191',
|
||||
'email' => 'nullable|email|max:191',
|
||||
'password' => 'nullable|string|min:8',
|
||||
'employee_num' => 'nullable|string|max:191',
|
||||
'jobtitle' => 'nullable|string|max:191',
|
||||
'phone' => 'nullable|string|max:35',
|
||||
'mobile' => 'nullable|string|max:35',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'department_id' => 'nullable|integer|exists:departments,id',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'manager_id' => 'nullable|integer|exists:users,id',
|
||||
'activated' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
'start_date' => 'nullable|date_format:Y-m-d',
|
||||
'end_date' => 'nullable|date_format:Y-m-d',
|
||||
'vip' => 'nullable|boolean',
|
||||
'remote' => 'nullable|boolean',
|
||||
'website' => 'nullable|url|max:191',
|
||||
'address' => 'nullable|string|max:191',
|
||||
'city' => 'nullable|string|max:191',
|
||||
'state' => 'nullable|string|max:191',
|
||||
'country' => 'nullable|string|max:191',
|
||||
'zip' => 'nullable|string|max:10',
|
||||
'group_ids' => 'nullable|array',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
if (User::where('username', $request->get('username'))->exists()) {
|
||||
return Response::make(Response::error(trans('mcp.username_taken', ['username' => $request->get('username')])));
|
||||
}
|
||||
|
||||
$user = new User;
|
||||
$user->fill($request->only([
|
||||
'first_name', 'last_name', 'username', 'email', 'employee_num',
|
||||
'jobtitle', 'phone', 'mobile', 'department_id', 'location_id',
|
||||
'manager_id', 'notes', 'start_date', 'end_date', 'vip', 'remote',
|
||||
'website', 'address', 'city', 'state', 'country', 'zip',
|
||||
]));
|
||||
|
||||
$user->activated = $request->filled('activated') ? (bool) $request->get('activated') : true;
|
||||
$user->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$user->created_by = auth()->id();
|
||||
|
||||
if ($request->filled('password')) {
|
||||
$user->password = bcrypt($request->get('password'));
|
||||
} else {
|
||||
$user->password = $user->noPassword();
|
||||
}
|
||||
|
||||
if ($user->save()) {
|
||||
$groupIds = [];
|
||||
if ($request->filled('group_ids') && auth()->user()->isSuperUser()) {
|
||||
$groupIds = Group::whereIn('id', $request->get('group_ids'))->pluck('id')->all();
|
||||
$user->groups()->sync($groupIds);
|
||||
} elseif ($request->filled('group_ids')) {
|
||||
return Response::make(Response::error(trans('mcp.superadmin_required_for_groups')));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.user_created', ['username' => $user->username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.user_created', ['username' => $user->username]),
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'email' => $user->email,
|
||||
'first_name' => $user->first_name,
|
||||
'last_name' => $user->last_name,
|
||||
'group_ids' => $groupIds,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $user->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'first_name' => $schema->string()->description('First name (required)'),
|
||||
'last_name' => $schema->string()->description('Last name'),
|
||||
'username' => $schema->string()->description('Username (required, must be unique)'),
|
||||
'email' => $schema->string()->description('Email address'),
|
||||
'password' => $schema->string()->description('Password (min 8 characters; if omitted, account will have no password set)'),
|
||||
'employee_num' => $schema->string()->description('Employee number'),
|
||||
'jobtitle' => $schema->string()->description('Job title'),
|
||||
'phone' => $schema->string()->description('Phone number'),
|
||||
'mobile' => $schema->string()->description('Mobile number'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'department_id' => $schema->number()->description('Department ID'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'manager_id' => $schema->number()->description('Manager user ID'),
|
||||
'activated' => $schema->boolean()->description('Whether the account is active (default: true)'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'start_date' => $schema->string()->description('Employment start date (YYYY-MM-DD)'),
|
||||
'end_date' => $schema->string()->description('Employment end date (YYYY-MM-DD)'),
|
||||
'vip' => $schema->boolean()->description('Mark user as VIP'),
|
||||
'remote' => $schema->boolean()->description('Mark user as remote'),
|
||||
'website' => $schema->string()->description('Website URL'),
|
||||
'address' => $schema->string()->description('Street address'),
|
||||
'city' => $schema->string()->description('City'),
|
||||
'state' => $schema->string()->description('State/province'),
|
||||
'country' => $schema->string()->description('Country'),
|
||||
'zip' => $schema->string()->description('Postal/ZIP code'),
|
||||
'group_ids' => $schema->array()->description('Array of permission group IDs to assign (requires superadmin). Example: [1, 3]'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the user was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new user'),
|
||||
'username' => $schema->string()->description('Username of the new user'),
|
||||
'email' => $schema->string()->description('Email of the new user'),
|
||||
'first_name' => $schema->string()->description('First name'),
|
||||
'last_name' => $schema->string()->description('Last name'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_accessory')]
|
||||
#[Title('Delete Accessory')]
|
||||
#[Description('Soft-delete a Snipe-IT accessory. The accessory must have no units currently checked out.')]
|
||||
class DeleteAccessoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$accessory = $this->resolveAccessory($request);
|
||||
|
||||
if (! $accessory) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $accessory)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($accessory->numCheckedOut() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_has_checkouts')));
|
||||
}
|
||||
|
||||
$name = $accessory->name;
|
||||
|
||||
$accessory->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.accessory_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.accessory_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAccessory(Request $request): ?Accessory
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Accessory::withCount('checkouts as checkouts_count')->find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Accessory::withCount('checkouts as checkouts_count')->where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the accessory to delete'),
|
||||
'name' => $schema->string()->description('Name of the accessory 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 accessory'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\AssetModel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_asset_model')]
|
||||
#[Title('Delete Asset Model')]
|
||||
#[Description('Soft-delete a Snipe-IT asset model by numeric ID or name')]
|
||||
class DeleteAssetModelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$model = $this->resolveModel($request);
|
||||
|
||||
if (! $model) {
|
||||
return Response::make(Response::error(trans('mcp.asset_model_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $model)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($model->assets()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.model_has_assets')));
|
||||
}
|
||||
|
||||
$name = $model->name;
|
||||
|
||||
$model->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_model_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_model_deleted', ['name' => $name]),
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_asset')]
|
||||
#[Title('Delete Asset')]
|
||||
#[Description('Soft-delete a Snipe-IT asset. If the asset is currently checked out it will be checked in first.')]
|
||||
class DeleteAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'id' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$assetTag = $asset->asset_tag;
|
||||
|
||||
if ($asset->assignedTo) {
|
||||
$target = $asset->assignedTo;
|
||||
$originalValues = $asset->getRawOriginal();
|
||||
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checked in on delete', date('Y-m-d H:i:s'), $originalValues));
|
||||
DB::table('assets')->where('id', $asset->id)->update(['assigned_to' => null]);
|
||||
}
|
||||
|
||||
$asset->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_deleted', ['asset_tag' => $assetTag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_deleted', ['asset_tag' => $assetTag]),
|
||||
'asset_tag' => $assetTag,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))->first();
|
||||
}
|
||||
if ($request->filled('serial')) {
|
||||
return Asset::where('serial', $request->get('serial'))->first();
|
||||
}
|
||||
if ($request->filled('id')) {
|
||||
return Asset::find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset to delete'),
|
||||
'serial' => $schema->string()->description('Serial number of the asset to delete'),
|
||||
'id' => $schema->number()->description('Numeric ID of the asset to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'error' => $schema->boolean()->description('True if the deletion failed'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the deleted asset'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_category')]
|
||||
#[Title('Delete Category')]
|
||||
#[Description('Soft-delete a Snipe-IT category. The category must have no items assigned to it.')]
|
||||
class DeleteCategoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$category = $this->resolveCategory($request);
|
||||
|
||||
if (! $category) {
|
||||
return Response::make(Response::error(trans('mcp.category_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $category)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $category->name;
|
||||
|
||||
try {
|
||||
$category->delete();
|
||||
} catch (\Exception $e) {
|
||||
return Response::make(Response::error(trans('mcp.category_delete_failed', ['error' => $e->getMessage()])));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.category_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.category_deleted', ['name' => $name]),
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_company')]
|
||||
#[Title('Delete Company')]
|
||||
#[Description('Soft-delete a Snipe-IT company by numeric ID or name')]
|
||||
class DeleteCompanyTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$company = $this->resolveCompany($request);
|
||||
|
||||
if (! $company) {
|
||||
return Response::make(Response::error(trans('mcp.company_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $company)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $company->name;
|
||||
|
||||
$company->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.company_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.company_deleted', ['name' => $name]),
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Component;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_component')]
|
||||
#[Title('Delete Component')]
|
||||
#[Description('Soft-delete a Snipe-IT component. The component must have no units currently checked out to assets.')]
|
||||
class DeleteComponentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:191',
|
||||
]);
|
||||
|
||||
$component = $this->resolveComponent($request);
|
||||
|
||||
if (! $component) {
|
||||
return Response::make(Response::error(trans('mcp.component_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $component)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($component->numCheckedOut() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.component_has_checkouts')));
|
||||
}
|
||||
|
||||
$name = $component->name;
|
||||
|
||||
$component->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.component_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.component_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveComponent(Request $request): ?Component
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Component::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Component::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the component to delete'),
|
||||
'name' => $schema->string()->description('Name of the component 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 component'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Consumable;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_consumable')]
|
||||
#[Title('Delete Consumable')]
|
||||
#[Description('Soft-delete a Snipe-IT consumable. The consumable must have no units currently checked out.')]
|
||||
class DeleteConsumableTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$consumable = $this->resolveConsumable($request);
|
||||
|
||||
if (! $consumable) {
|
||||
return Response::make(Response::error(trans('mcp.consumable_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $consumable)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($consumable->users()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.consumable_has_checkouts')));
|
||||
}
|
||||
|
||||
$name = $consumable->name;
|
||||
|
||||
$consumable->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.consumable_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.consumable_deleted', ['name' => $name]),
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Department;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_department')]
|
||||
#[Title('Delete Department')]
|
||||
#[Description('Soft-delete a Snipe-IT department. The department must have no users assigned to it.')]
|
||||
class DeleteDepartmentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$department = $this->resolveDepartment($request);
|
||||
|
||||
if (! $department) {
|
||||
return Response::make(Response::error(trans('mcp.department_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $department)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($department->users->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.department_has_users')));
|
||||
}
|
||||
|
||||
$name = $department->name;
|
||||
|
||||
$department->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.department_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.department_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveDepartment(Request $request): ?Department
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Department::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Department::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the department to delete'),
|
||||
'name' => $schema->string()->description('Name of the department 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 department'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Depreciation;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_depreciation')]
|
||||
#[Title('Delete Depreciation')]
|
||||
#[Description('Soft-delete a Snipe-IT depreciation schedule by numeric ID or name')]
|
||||
class DeleteDepreciationTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$dep = $this->resolveDepreciation($request);
|
||||
|
||||
if (! $dep) {
|
||||
return Response::make(Response::error(trans('mcp.depreciation_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $dep)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $dep->name;
|
||||
|
||||
$dep->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.depreciation_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.depreciation_deleted', ['name' => $name]),
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_group')]
|
||||
#[Title('Delete Group')]
|
||||
#[Description('Delete a Snipe-IT permission group by ID or name. The group must have no users assigned.')]
|
||||
class DeleteGroupTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('superadmin')) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->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(trans('mcp.id_or_name_required')));
|
||||
}
|
||||
|
||||
if (! $group) {
|
||||
return Response::make(Response::error(trans('mcp.group_not_found')));
|
||||
}
|
||||
|
||||
$groupName = $group->name;
|
||||
|
||||
if ($group->delete()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.group_deleted', ['name' => $groupName]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.group_deleted', ['name' => $groupName]),
|
||||
'name' => $groupName,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\License;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_license')]
|
||||
#[Title('Delete License')]
|
||||
#[Description('Soft-delete a Snipe-IT license. The license must have no seats currently assigned to users or assets.')]
|
||||
class DeleteLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$license = $this->resolveLicense($request);
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error(trans('mcp.license_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $license)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($license->assignedCount()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.license_has_seats_assigned')));
|
||||
}
|
||||
|
||||
$name = $license->name;
|
||||
|
||||
DB::table('license_seats')
|
||||
->where('license_id', $license->id)
|
||||
->update(['assigned_to' => null, 'asset_id' => null]);
|
||||
|
||||
$license->licenseseats()->delete();
|
||||
$license->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveLicense(Request $request): ?License
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return License::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return License::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the license to delete'),
|
||||
'name' => $schema->string()->description('Name of the license to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted license'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Location;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_location')]
|
||||
#[Title('Delete Location')]
|
||||
#[Description('Soft-delete a Snipe-IT location by numeric ID or name')]
|
||||
class DeleteLocationTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$location = $this->resolveLocation($request);
|
||||
|
||||
if (! $location) {
|
||||
return Response::make(Response::error(trans('mcp.location_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $location)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($location->users()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.location_has_users')));
|
||||
}
|
||||
|
||||
if ($location->children()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.location_has_child_locations')));
|
||||
}
|
||||
|
||||
$name = $location->name;
|
||||
|
||||
$location->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.location_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.location_deleted', ['name' => $name]),
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Manufacturer;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_manufacturer')]
|
||||
#[Title('Delete Manufacturer')]
|
||||
#[Description('Soft-delete a Snipe-IT manufacturer identified by numeric ID or name')]
|
||||
class DeleteManufacturerTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$manufacturer = $this->resolveManufacturer($request);
|
||||
|
||||
if (! $manufacturer) {
|
||||
return Response::make(Response::error(trans('mcp.manufacturer_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $manufacturer)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $manufacturer->name;
|
||||
|
||||
$manufacturer->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.manufacturer_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.manufacturer_deleted', ['name' => $name]),
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Statuslabel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_status_label')]
|
||||
#[Title('Delete Status Label')]
|
||||
#[Description('Soft-delete a Snipe-IT status label identified by numeric ID or name')]
|
||||
class DeleteStatusLabelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$label = $this->resolveStatusLabel($request);
|
||||
|
||||
if (! $label) {
|
||||
return Response::make(Response::error(trans('mcp.status_label_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $label)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($label->assets()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.status_label_has_assets')));
|
||||
}
|
||||
|
||||
$name = $label->name;
|
||||
|
||||
$label->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.status_label_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.status_label_deleted', ['name' => $name]),
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Supplier;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_supplier')]
|
||||
#[Title('Delete Supplier')]
|
||||
#[Description('Soft-delete a Snipe-IT supplier identified by numeric ID or name')]
|
||||
class DeleteSupplierTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$supplier = $this->resolveSupplier($request);
|
||||
|
||||
if (! $supplier) {
|
||||
return Response::make(Response::error(trans('mcp.supplier_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $supplier)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $supplier->name;
|
||||
|
||||
$supplier->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.supplier_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.supplier_deleted', ['name' => $name]),
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_user')]
|
||||
#[Title('Delete User')]
|
||||
#[Description('Soft-delete a Snipe-IT user. The user must have no assets, licenses, accessories, or consumables assigned.')]
|
||||
class DeleteUserTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'username' => 'nullable|string|max:191',
|
||||
'email' => 'nullable|string|max:191',
|
||||
]);
|
||||
|
||||
$user = $this->resolveUser($request);
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $user)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($user->id === auth()->id()) {
|
||||
return Response::make(Response::error(trans('mcp.user_cannot_delete_self')));
|
||||
}
|
||||
|
||||
if ($user->allAssignedCount() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.user_has_items')));
|
||||
}
|
||||
|
||||
$username = $user->username;
|
||||
|
||||
if ($user->delete()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.user_deleted', ['username' => $username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.user_deleted', ['username' => $username]),
|
||||
'username' => $username,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.delete_failed_error', ['error' => $user->getErrors()->first()])));
|
||||
}
|
||||
|
||||
private function resolveUser(Request $request): ?User
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return User::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('username')) {
|
||||
return User::where('username', $request->get('username'))->first();
|
||||
}
|
||||
if ($request->filled('email')) {
|
||||
return User::where('email', $request->get('email'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric user ID to delete'),
|
||||
'username' => $schema->string()->description('Username of the user to delete'),
|
||||
'email' => $schema->string()->description('Email address of the user 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(),
|
||||
'username' => $schema->string()->description('Username of the deleted user'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Actionlog;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('get_activity_log')]
|
||||
#[Title('Get Activity Log')]
|
||||
#[Description('Retrieve the Snipe-IT activity log with optional filtering by item type, item ID, user, and action type')]
|
||||
class GetActivityLogTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('activity.view')) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->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(trans('mcp.list_activity', ['total' => $total, 'count' => 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('get_current_user')]
|
||||
#[Title('Get Current User')]
|
||||
#[Description('Return information about the currently authenticated Snipe-IT user')]
|
||||
class GetCurrentUserTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! auth()->check()) {
|
||||
return Response::make(Response::error(trans('mcp.not_authenticated')));
|
||||
}
|
||||
|
||||
$user = User::with('company', 'department', 'userloc')->find(auth()->id());
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.not_authenticated')));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.current_user', ['username' => $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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('get_user_assets')]
|
||||
#[Title('Get User Assets')]
|
||||
#[Description('Return all assets currently checked out to a Snipe-IT user')]
|
||||
class GetUserAssetsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', User::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$user = User::find($request->get('id'));
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.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(trans('mcp.user_assets_found', ['count' => count($data), 'username' => $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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\AssetModel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_asset_models')]
|
||||
#[Title('List Asset Models')]
|
||||
#[Description('Search and list Snipe-IT asset models with optional filtering and pagination')]
|
||||
class ListAssetModelsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', AssetModel::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->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(trans('mcp.list_asset_models', ['total' => $total, 'count' => 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_asset_notes')]
|
||||
#[Title('List Asset Notes')]
|
||||
#[Description('List manual notes added to a Snipe-IT asset, identified by asset tag, serial number, or numeric ID')]
|
||||
class ListAssetNotesTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|string|max:100',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'id' => 'nullable|integer',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$query = Actionlog::with('adminuser:id,username')
|
||||
->where('item_type', Asset::class)
|
||||
->where('item_id', $asset->id)
|
||||
->where('action_type', 'note added')
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$records = $query->skip($offset)->take($limit)
|
||||
->get(['id', 'created_at', 'note', 'created_by', 'item_id', 'action_type']);
|
||||
|
||||
$notes = $records->map(fn ($n) => [
|
||||
'id' => $n->id,
|
||||
'created_at' => $n->created_at?->toISOString(),
|
||||
'note' => $n->note,
|
||||
'created_by_id' => $n->created_by,
|
||||
'created_by_username' => $n->adminuser?->username,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_asset_notes', [
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'total' => $total,
|
||||
'count' => count($notes),
|
||||
]))
|
||||
)->withStructuredContent([
|
||||
'asset_id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'notes' => $notes,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))->first();
|
||||
}
|
||||
if ($request->filled('serial')) {
|
||||
return Asset::where('serial', $request->get('serial'))->first();
|
||||
}
|
||||
if ($request->filled('id')) {
|
||||
return Asset::find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset'),
|
||||
'serial' => $schema->string()->description('Serial number of the asset'),
|
||||
'id' => $schema->number()->description('Numeric ID of the asset'),
|
||||
'limit' => $schema->number()->description('Number of notes to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of notes to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_id' => $schema->number()->description('Numeric ID of the asset')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset')->required(),
|
||||
'total' => $schema->number()->description('Total number of notes on this asset')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'notes' => $schema->array()->description('List of notes'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_assets')]
|
||||
#[Title('List Assets')]
|
||||
#[Description('Search and list Snipe-IT assets with optional filtering by keyword, status, and pagination')]
|
||||
class ListAssetsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('index', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'status_type' => 'nullable|string|in:RTD,Deployed,Archived,Pending,Undeployable',
|
||||
'company_id' => 'nullable|integer',
|
||||
'location_id' => 'nullable|integer',
|
||||
'category_id' => 'nullable|integer',
|
||||
'model_id' => 'nullable|integer',
|
||||
'manufacturer_id' => 'nullable|integer',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$assets = Asset::select('assets.*')
|
||||
->with('status', 'assignedTo', 'model.category', 'model.manufacturer', 'location', 'company');
|
||||
|
||||
match ($request->filled('status_type') ? $request->get('status_type') : null) {
|
||||
'RTD' => $assets->rtd(),
|
||||
'Deployed' => $assets->deployed(),
|
||||
'Archived' => $assets->archived(),
|
||||
'Pending' => $assets->pending(),
|
||||
'Undeployable' => $assets->undeployable(),
|
||||
default => $assets->notArchived(),
|
||||
};
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$assets->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$assets->where('assets.company_id', '=', $request->get('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$assets->where('assets.location_id', '=', $request->get('location_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$assets->inCategory($request->get('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('model_id')) {
|
||||
$assets->inModels([$request->get('model_id')]);
|
||||
}
|
||||
|
||||
if ($request->filled('manufacturer_id')) {
|
||||
$assets->byManufacturer($request->get('manufacturer_id'));
|
||||
}
|
||||
|
||||
$assets->orderBy('assets.created_at', 'desc');
|
||||
|
||||
$total = $assets->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $assets->skip($offset)->take($limit)->get();
|
||||
|
||||
$assetsData = $results->map(fn (Asset $asset) => [
|
||||
'id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'name' => $asset->name,
|
||||
'serial' => $asset->serial,
|
||||
'status' => $asset->status?->name,
|
||||
'status_type' => $asset->status?->getStatuslabelType(),
|
||||
'model' => $asset->model?->name,
|
||||
'category' => $asset->model?->category?->name,
|
||||
'manufacturer' => $asset->model?->manufacturer?->name,
|
||||
'company' => $asset->company?->name,
|
||||
'location' => $asset->location?->name,
|
||||
'assigned_to_id' => $asset->assigned_to,
|
||||
'assigned_to_type' => $asset->assigned_type ? class_basename($asset->assigned_type) : null,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_assets', ['total' => $total, 'count' => count($assetsData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'assets' => $assetsData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()
|
||||
->description('Keyword to search across asset tag, serial, name, and model'),
|
||||
'status_type' => $schema->string()
|
||||
->description('Filter by status type: RTD (ready to deploy), Deployed, Archived, Pending, or Undeployable'),
|
||||
'company_id' => $schema->number()
|
||||
->description('Filter by company ID'),
|
||||
'location_id' => $schema->number()
|
||||
->description('Filter by location ID'),
|
||||
'category_id' => $schema->number()
|
||||
->description('Filter by category ID'),
|
||||
'model_id' => $schema->number()
|
||||
->description('Filter by model 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 assets')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_categories')]
|
||||
#[Title('List Categories')]
|
||||
#[Description('Search and list Snipe-IT categories with optional filtering by type and pagination')]
|
||||
class ListCategoriesTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Category::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->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(trans('mcp.list_categories', ['total' => $total, 'count' => 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_companies')]
|
||||
#[Title('List Companies')]
|
||||
#[Description('Search and list Snipe-IT companies with optional filtering and pagination')]
|
||||
class ListCompaniesTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Company::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->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(trans('mcp.list_companies', ['total' => $total, 'count' => 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Consumable;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_consumables')]
|
||||
#[Title('List Consumables')]
|
||||
#[Description('Search and list Snipe-IT consumables with optional filtering by keyword, company, category, manufacturer, location, and pagination')]
|
||||
class ListConsumablesTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('index', Consumable::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->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(trans('mcp.list_consumables', ['total' => $total, 'count' => 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Depreciation;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_depreciations')]
|
||||
#[Title('List Depreciations')]
|
||||
#[Description('Search and list Snipe-IT depreciation schedules with optional filtering and pagination')]
|
||||
class ListDepreciationsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Depreciation::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->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(trans('mcp.list_depreciations', ['total' => $total, 'count' => 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_groups')]
|
||||
#[Title('List Groups')]
|
||||
#[Description('List Snipe-IT permission groups with optional search and pagination')]
|
||||
class ListGroupsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('superadmin')) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->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(trans('mcp.list_groups', ['total' => $total, 'count' => 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Component;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\License;
|
||||
use App\Models\Location;
|
||||
use App\Models\Maintenance;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_history')]
|
||||
#[Title('List History')]
|
||||
#[Description('List the activity history for a Snipe-IT object. Supported types: accessory, asset, asset_model, component, consumable, license, location, maintenance, user')]
|
||||
class ListHistoryTool extends Tool
|
||||
{
|
||||
private const TYPE_MAP = [
|
||||
'accessory' => Accessory::class,
|
||||
'asset' => Asset::class,
|
||||
'asset_model' => AssetModel::class,
|
||||
'component' => Component::class,
|
||||
'consumable' => Consumable::class,
|
||||
'license' => License::class,
|
||||
'location' => Location::class,
|
||||
'maintenance' => Maintenance::class,
|
||||
'user' => User::class,
|
||||
];
|
||||
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$validTypes = implode(',', array_keys(self::TYPE_MAP));
|
||||
|
||||
$request->validate([
|
||||
'object_type' => 'required|string|in:'.$validTypes,
|
||||
'id' => 'required|integer|min:1',
|
||||
'search' => 'nullable|string|max:255',
|
||||
'action_type' => 'nullable|string|max:100',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$objectType = $request->get('object_type');
|
||||
$modelClass = self::TYPE_MAP[$objectType];
|
||||
|
||||
$object = $modelClass::withTrashed()->find($request->get('id'));
|
||||
|
||||
if (! $object) {
|
||||
return Response::make(Response::error(trans('mcp.object_not_found', ['type' => $objectType])));
|
||||
}
|
||||
|
||||
if (! Gate::allows('history', $object)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$modelClass = get_class($object);
|
||||
$modelId = $object->getKey();
|
||||
|
||||
// Wrap the item/target OR in a subquery so additional filters apply to both sides.
|
||||
$history = Actionlog::where(function ($q) use ($modelClass, $modelId) {
|
||||
$q->where('item_type', $modelClass)
|
||||
->where('item_id', $modelId)
|
||||
->orWhere(function ($q2) use ($modelClass, $modelId) {
|
||||
$q2->where('target_type', $modelClass)
|
||||
->where('target_id', $modelId);
|
||||
});
|
||||
});
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$history->TextSearch(e($request->get('search')));
|
||||
}
|
||||
|
||||
if ($request->filled('action_type')) {
|
||||
$history->where('action_type', $request->get('action_type'));
|
||||
}
|
||||
|
||||
$history->orderBy('action_logs.created_at', 'desc');
|
||||
|
||||
$total = (clone $history)->count();
|
||||
$records = $history->skip($offset)->take($limit)->forApiHistory()->get();
|
||||
|
||||
$entries = $records->map(fn ($log) => [
|
||||
'id' => $log->id,
|
||||
'action_type' => $log->action_type,
|
||||
'created_at' => $log->created_at?->toISOString(),
|
||||
'note' => $log->note,
|
||||
'created_by' => $log->adminuser ? [
|
||||
'id' => $log->adminuser->id,
|
||||
'username' => $log->adminuser->username,
|
||||
] : null,
|
||||
'target' => $log->target ? [
|
||||
'id' => $log->target->getKey(),
|
||||
'type' => class_basename($log->target_type),
|
||||
'name' => $log->target->present()->name() ?? null,
|
||||
] : null,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_history', [
|
||||
'total' => $total,
|
||||
'count' => count($entries),
|
||||
'type' => $objectType,
|
||||
]))
|
||||
)->withStructuredContent([
|
||||
'object_type' => $objectType,
|
||||
'object_id' => $object->id,
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'history' => $entries,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'object_type' => $schema->string()->description('Type of object: accessory, asset, asset_model, component, consumable, license, location, maintenance, user'),
|
||||
'id' => $schema->number()->description('Numeric ID of the object'),
|
||||
'search' => $schema->string()->description('Filter history by keyword'),
|
||||
'action_type' => $schema->string()->description('Filter by action type (e.g. checkout, checkin, update, note added, uploaded)'),
|
||||
'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 [
|
||||
'object_type' => $schema->string()->description('Type of object queried')->required(),
|
||||
'object_id' => $schema->number()->description('ID of the object queried')->required(),
|
||||
'total' => $schema->number()->description('Total number of history entries')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'history' => $schema->array()->description('List of history entries'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\License;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_licenses')]
|
||||
#[Title('List Licenses')]
|
||||
#[Description('Search and list Snipe-IT licenses with optional filtering by keyword, company, category, and manufacturer')]
|
||||
class ListLicensesTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('index', License::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'company_id' => 'nullable|integer',
|
||||
'category_id' => 'nullable|integer',
|
||||
'manufacturer_id' => 'nullable|integer',
|
||||
'supplier_id' => 'nullable|integer',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$licenses = License::with('company', 'manufacturer', 'supplier', 'category')
|
||||
->withCount('freeSeats as free_seats_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$licenses->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$licenses->where('licenses.company_id', '=', $request->get('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$licenses->where('category_id', '=', $request->get('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('manufacturer_id')) {
|
||||
$licenses->where('manufacturer_id', '=', $request->get('manufacturer_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('supplier_id')) {
|
||||
$licenses->where('supplier_id', '=', $request->get('supplier_id'));
|
||||
}
|
||||
|
||||
$licenses->orderBy('licenses.created_at', 'desc');
|
||||
|
||||
$total = $licenses->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $licenses->skip($offset)->take($limit)->get();
|
||||
|
||||
$licensesData = $results->map(fn (License $license) => [
|
||||
'id' => $license->id,
|
||||
'name' => $license->name,
|
||||
'serial' => $license->serial,
|
||||
'seats' => $license->seats,
|
||||
'free_seats' => $license->free_seats_count,
|
||||
'category' => $license->category?->name,
|
||||
'manufacturer' => $license->manufacturer?->name,
|
||||
'company' => $license->company?->name,
|
||||
'supplier' => $license->supplier?->name,
|
||||
'expiration_date' => $license->expiration_date?->format('Y-m-d'),
|
||||
'purchase_date' => $license->purchase_date?->format('Y-m-d'),
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_licenses', ['total' => $total, 'count' => count($licensesData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'licenses' => $licensesData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across name, serial, notes, and order number'),
|
||||
'company_id' => $schema->number()->description('Filter by company ID'),
|
||||
'category_id' => $schema->number()->description('Filter by category ID'),
|
||||
'manufacturer_id' => $schema->number()->description('Filter by manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Filter by supplier ID'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching licenses')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Location;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_locations')]
|
||||
#[Title('List Locations')]
|
||||
#[Description('Search and list Snipe-IT locations with optional filtering and pagination')]
|
||||
class ListLocationsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Location::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->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(trans('mcp.list_locations', ['total' => $total, 'count' => 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Maintenance;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_maintenances')]
|
||||
#[Title('List Maintenances')]
|
||||
#[Description('List asset maintenances with optional filtering by asset and pagination')]
|
||||
class ListMaintenancesTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->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(trans('mcp.list_maintenances', ['total' => $total, 'count' => 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Manufacturer;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_manufacturers')]
|
||||
#[Title('List Manufacturers')]
|
||||
#[Description('Search and list Snipe-IT manufacturers with optional filtering and pagination')]
|
||||
class ListManufacturersTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Manufacturer::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->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(trans('mcp.list_manufacturers', ['total' => $total, 'count' => 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Statuslabel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_status_labels')]
|
||||
#[Title('List Status Labels')]
|
||||
#[Description('Search and list Snipe-IT status labels with optional filtering and pagination')]
|
||||
class ListStatusLabelsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Statuslabel::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->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(trans('mcp.list_status_labels', ['total' => $total, 'count' => 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Supplier;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_suppliers')]
|
||||
#[Title('List Suppliers')]
|
||||
#[Description('Search and list Snipe-IT suppliers with optional filtering and pagination')]
|
||||
class ListSuppliersTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Supplier::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->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(trans('mcp.list_suppliers', ['total' => $total, 'count' => 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Company;
|
||||
use App\Models\Component;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\Department;
|
||||
use App\Models\License;
|
||||
use App\Models\Location;
|
||||
use App\Models\Maintenance;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_uploads')]
|
||||
#[Title('List Uploads')]
|
||||
#[Description('List files uploaded to a Snipe-IT object. Supported types: accessories, assets, companies, components, consumables, departments, licenses, locations, maintenances, models, suppliers, users')]
|
||||
class ListUploadsTool extends Tool
|
||||
{
|
||||
private const TYPE_MAP = [
|
||||
'accessories' => Accessory::class,
|
||||
'assets' => Asset::class,
|
||||
'companies' => Company::class,
|
||||
'components' => Component::class,
|
||||
'consumables' => Consumable::class,
|
||||
'departments' => Department::class,
|
||||
'licenses' => License::class,
|
||||
'locations' => Location::class,
|
||||
'maintenances' => Maintenance::class,
|
||||
'models' => AssetModel::class,
|
||||
'suppliers' => Supplier::class,
|
||||
'users' => User::class,
|
||||
];
|
||||
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$validTypes = implode(',', array_keys(self::TYPE_MAP));
|
||||
|
||||
$request->validate([
|
||||
'object_type' => 'required|string|in:'.$validTypes,
|
||||
'id' => 'required|integer|min:1',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$objectType = $request->get('object_type');
|
||||
$modelClass = self::TYPE_MAP[$objectType];
|
||||
|
||||
$object = $modelClass::withTrashed()->find($request->get('id'));
|
||||
|
||||
if (! $object) {
|
||||
return Response::make(Response::error(trans('mcp.object_not_found', ['type' => $objectType])));
|
||||
}
|
||||
|
||||
if (! Gate::allows('files', $object)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$query = $object->uploads()->with('adminuser');
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$uploads = $query->skip($offset)->take($limit)->orderBy('created_at', 'desc')->get();
|
||||
|
||||
$files = $uploads->map(fn ($file) => [
|
||||
'id' => $file->id,
|
||||
'filename' => $file->filename,
|
||||
'url' => $file->uploads_file_url(),
|
||||
'note' => $file->note,
|
||||
'created_by' => $file->adminuser ? [
|
||||
'id' => $file->adminuser->id,
|
||||
'username' => $file->adminuser->username,
|
||||
] : null,
|
||||
'created_at' => $file->created_at?->toISOString(),
|
||||
'exists_on_disk' => Storage::exists($file->uploads_file_path()),
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_uploads', [
|
||||
'total' => $total,
|
||||
'count' => count($files),
|
||||
'type' => $objectType,
|
||||
]))
|
||||
)->withStructuredContent([
|
||||
'object_type' => $objectType,
|
||||
'object_id' => $object->id,
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'files' => $files,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'object_type' => $schema->string()->description('Type of object: accessories, assets, companies, components, consumables, departments, licenses, locations, maintenances, models, suppliers, users'),
|
||||
'id' => $schema->number()->description('Numeric ID of the object'),
|
||||
'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 [
|
||||
'object_type' => $schema->string()->description('Type of object queried')->required(),
|
||||
'object_id' => $schema->number()->description('ID of the object queried')->required(),
|
||||
'total' => $schema->number()->description('Total number of uploaded files')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'files' => $schema->array()->description('List of uploaded files'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_users')]
|
||||
#[Title('List Users')]
|
||||
#[Description('Search and list Snipe-IT users with optional filtering by keyword, company, department, and location')]
|
||||
class ListUsersTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('index', User::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'company_id' => 'nullable|integer',
|
||||
'department_id' => 'nullable|integer',
|
||||
'location_id' => 'nullable|integer',
|
||||
'activated' => 'nullable|boolean',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$users = User::with('company', 'department', 'userloc', 'manager')
|
||||
->withCount(['assets as assets_count', 'licenses as licenses_count']);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$users->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$users->where('users.company_id', '=', $request->get('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('department_id')) {
|
||||
$users->where('users.department_id', '=', $request->get('department_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$users->where('users.location_id', '=', $request->get('location_id'));
|
||||
}
|
||||
|
||||
if ($request->has('activated')) {
|
||||
$users->where('users.activated', '=', $request->get('activated'));
|
||||
}
|
||||
|
||||
$users->orderBy('users.last_name', 'asc')->orderBy('users.first_name', 'asc');
|
||||
|
||||
$total = $users->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $users->skip($offset)->take($limit)->get();
|
||||
|
||||
$usersData = $results->map(fn (User $user) => [
|
||||
'id' => $user->id,
|
||||
'first_name' => $user->first_name,
|
||||
'last_name' => $user->last_name,
|
||||
'username' => $user->username,
|
||||
'email' => $user->email,
|
||||
'jobtitle' => $user->jobtitle,
|
||||
'company' => $user->company?->name,
|
||||
'department' => $user->department?->name,
|
||||
'location' => $user->userloc?->name,
|
||||
'manager' => $user->manager ? trim($user->manager->first_name.' '.$user->manager->last_name) : null,
|
||||
'activated' => (bool) $user->activated,
|
||||
'assets_count' => $user->assets_count,
|
||||
'licenses_count' => $user->licenses_count,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_users', ['total' => $total, 'count' => count($usersData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'users' => $usersData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across name, username, email, and employee number'),
|
||||
'company_id' => $schema->number()->description('Filter by company ID'),
|
||||
'department_id' => $schema->number()->description('Filter by department ID'),
|
||||
'location_id' => $schema->number()->description('Filter by location ID'),
|
||||
'activated' => $schema->boolean()->description('Filter by account activated status'),
|
||||
'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 users')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('reset_2fa')]
|
||||
#[Title('Reset Two-Factor Authentication')]
|
||||
#[Description('Reset two-factor authentication for a Snipe-IT user')]
|
||||
class Reset2FATool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('update', User::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$user = User::find($request->get('id'));
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.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(trans('mcp.two_factor_reset', ['username' => $user->username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.two_factor_reset', ['username' => $user->username]),
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('restore_asset')]
|
||||
#[Title('Restore Asset')]
|
||||
#[Description('Restore a soft-deleted Snipe-IT asset')]
|
||||
class RestoreAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$asset = Asset::withTrashed()->find($request->get('id'));
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! $asset->deleted_at) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_deleted')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$asset->restore();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_restored', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_restored', ['asset_tag' => $asset->asset_tag]),
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('restore_user')]
|
||||
#[Title('Restore User')]
|
||||
#[Description('Restore a soft-deleted Snipe-IT user')]
|
||||
class RestoreUserTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('delete', User::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$user = User::withTrashed()->find($request->get('id'));
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
|
||||
if (! $user->deleted_at) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_deleted')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', User::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$user->restore();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.user_restored', ['username' => $user->username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.user_restored', ['username' => $user->username]),
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('send_password_reset')]
|
||||
#[Title('Send Password Reset Email')]
|
||||
#[Description('Send a password reset link to a Snipe-IT user identified by numeric ID, username, or email address. The user must be active, have an email address, and not be an LDAP-imported account.')]
|
||||
class SendPasswordResetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'username' => 'nullable|string|max:191',
|
||||
'email' => 'nullable|string|max:191',
|
||||
]);
|
||||
|
||||
$user = $this->resolveUser($request);
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', $user)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if (! $user->activated) {
|
||||
return Response::make(Response::error(trans('mcp.password_reset_user_inactive', ['username' => $user->username])));
|
||||
}
|
||||
|
||||
if (empty($user->email)) {
|
||||
return Response::make(Response::error(trans('mcp.password_reset_no_email', ['username' => $user->username])));
|
||||
}
|
||||
|
||||
if ($user->ldap_import) {
|
||||
return Response::make(Response::error(trans('mcp.password_reset_ldap_user', ['username' => $user->username])));
|
||||
}
|
||||
|
||||
try {
|
||||
$result = Password::sendResetLink(['email' => trim($user->email)]);
|
||||
} catch (\Exception $e) {
|
||||
return Response::make(Response::error(trans('mcp.password_reset_send_failed', ['error' => $e->getMessage()])));
|
||||
}
|
||||
|
||||
if ($result === Password::RESET_LINK_SENT) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.password_reset_sent', ['email' => $user->email]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.password_reset_sent', ['email' => $user->email]),
|
||||
'username' => $user->username,
|
||||
'email' => $user->email,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.password_reset_send_failed', ['error' => $result])));
|
||||
}
|
||||
|
||||
private function resolveUser(Request $request): ?User
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return User::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('username')) {
|
||||
return User::where('username', $request->get('username'))->first();
|
||||
}
|
||||
if ($request->filled('email')) {
|
||||
return User::where('email', $request->get('email'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the user'),
|
||||
'username' => $schema->string()->description('Username of the user'),
|
||||
'email' => $schema->string()->description('Email address of the user'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the reset email was sent'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'username' => $schema->string()->description('Username of the user'),
|
||||
'email' => $schema->string()->description('Email address the reset link was sent to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\AssetModel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('show_asset_model')]
|
||||
#[Title('Show Asset Model Details')]
|
||||
#[Description('Look up a single Snipe-IT asset model by numeric ID or name and return its full details')]
|
||||
class ShowAssetModelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$model = $this->resolveModel($request);
|
||||
|
||||
if ($model === false) {
|
||||
return Response::make(Response::error(trans('mcp.id_or_name_required')));
|
||||
}
|
||||
|
||||
if (! $model) {
|
||||
return Response::make(Response::error(trans('mcp.asset_model_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', $model)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$model->loadCount('assets as assets_count');
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_model_found', ['name' => $model->name]))
|
||||
)->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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('show_asset')]
|
||||
#[Title('Show Asset Details')]
|
||||
#[Description('Look up a single Snipe-IT asset by asset tag or numeric ID and return its full details')]
|
||||
class ShowAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'id' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$with = ['status', 'model.category', 'model.manufacturer', 'location', 'defaultLoc', 'company', 'supplier', 'adminuser'];
|
||||
$asset = null;
|
||||
|
||||
if ($request->filled('asset_tag')) {
|
||||
$asset = Asset::where('asset_tag', $request->get('asset_tag'))->with($with)->first();
|
||||
} elseif ($request->filled('serial')) {
|
||||
$asset = Asset::where('serial', $request->get('serial'))->with($with)->first();
|
||||
} elseif ($request->filled('id')) {
|
||||
$asset = Asset::with($with)->find($request->get('id'));
|
||||
}
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(
|
||||
Response::error(trans('mcp.asset_not_found'))
|
||||
);
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_found', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent($this->formatAsset($asset));
|
||||
}
|
||||
|
||||
private function formatAsset(Asset $asset): array
|
||||
{
|
||||
return [
|
||||
'id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'name' => $asset->name,
|
||||
'serial' => $asset->serial,
|
||||
'status' => $asset->status?->name,
|
||||
'status_type' => $asset->status?->getStatuslabelType(),
|
||||
'model' => $asset->model?->name,
|
||||
'model_number' => $asset->model?->model_number,
|
||||
'category' => $asset->model?->category?->name,
|
||||
'manufacturer' => $asset->model?->manufacturer?->name,
|
||||
'company' => $asset->company?->name,
|
||||
'location' => $asset->location?->name,
|
||||
'rtd_location' => $asset->defaultLoc?->name,
|
||||
'supplier' => $asset->supplier?->name,
|
||||
'assigned_to_id' => $asset->assigned_to,
|
||||
'assigned_to_type' => $asset->assigned_type ? class_basename($asset->assigned_type) : null,
|
||||
'notes' => $asset->notes,
|
||||
'order_number' => $asset->order_number,
|
||||
'purchase_date' => $asset->purchase_date?->format('Y-m-d'),
|
||||
'purchase_cost' => $asset->purchase_cost,
|
||||
'warranty_months' => $asset->warranty_months,
|
||||
'last_checkout' => $asset->last_checkout,
|
||||
'last_checkin' => $asset->last_checkin,
|
||||
'expected_checkin' => $asset->expected_checkin,
|
||||
'last_audit_date' => $asset->last_audit_date,
|
||||
'created_at' => $asset->created_at?->format('Y-m-d H:i:s'),
|
||||
'updated_at' => $asset->updated_at?->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()
|
||||
->description('The asset tag of the asset to look up'),
|
||||
'serial' => $schema->string()
|
||||
->description('The serial number of the asset to look up'),
|
||||
'id' => $schema->number()
|
||||
->description('The numeric ID of the asset to look up'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric asset ID'),
|
||||
'asset_tag' => $schema->string()->description('Asset tag'),
|
||||
'name' => $schema->string()->description('Asset name'),
|
||||
'serial' => $schema->string()->description('Serial number'),
|
||||
'status' => $schema->string()->description('Status label name'),
|
||||
'status_type' => $schema->string()->description('Status type: deployable, pending, or archived'),
|
||||
'model' => $schema->string()->description('Asset model name'),
|
||||
'model_number' => $schema->string()->description('Model number'),
|
||||
'category' => $schema->string()->description('Category name'),
|
||||
'manufacturer' => $schema->string()->description('Manufacturer name'),
|
||||
'company' => $schema->string()->description('Company name'),
|
||||
'location' => $schema->string()->description('Current location name'),
|
||||
'rtd_location' => $schema->string()->description('Default return-to-deploy location name'),
|
||||
'assigned_to_id' => $schema->number()->description('ID of the entity this asset is currently assigned to'),
|
||||
'assigned_to_type' => $schema->string()->description('Type of entity assigned to: User, Asset, or Location'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'purchase_cost' => $schema->string()->description('Purchase cost'),
|
||||
'last_checkout' => $schema->string()->description('Date of last checkout'),
|
||||
'last_checkin' => $schema->string()->description('Date of last checkin'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('show_category')]
|
||||
#[Title('Show Category Details')]
|
||||
#[Description('Look up a single Snipe-IT category by numeric ID or name and return its full details')]
|
||||
class ShowCategoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->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(trans('mcp.id_or_name_required')));
|
||||
}
|
||||
|
||||
if (! $category) {
|
||||
return Response::make(Response::error(trans('mcp.category_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', $category)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.category_found', ['name' => $category->name]))
|
||||
)->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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('show_company')]
|
||||
#[Title('Show Company Details')]
|
||||
#[Description('Look up a single Snipe-IT company by numeric ID or name and return its full details')]
|
||||
class ShowCompanyTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->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(trans('mcp.id_or_name_required')));
|
||||
}
|
||||
|
||||
if (! $company) {
|
||||
return Response::make(Response::error(trans('mcp.company_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', $company)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.company_found', ['name' => $company->name]))
|
||||
)->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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user