Compare commits
220 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 | |||
| 575e825579 | |||
| dc8cbf4786 | |||
| 5f81a48d8b | |||
| c22e4c00a5 | |||
| 9b5ead39d3 | |||
| 158e66f9c6 | |||
| bd8e944e2f | |||
| 06d95b679b | |||
| ff75b9eed8 | |||
| 17a88fcb80 | |||
| eca34de593 | |||
| 40e89756bf | |||
| 55e46b2d15 | |||
| 02383aad7b | |||
| e75f54cc1c | |||
| 3668c24d02 | |||
| a84533b4f4 | |||
| cbe750cc9e | |||
| a77dedf3d7 | |||
| b6ce823cc2 | |||
| f7e8ce2ade | |||
| 62e5b71dc1 | |||
| 3d04324595 | |||
| 468cf73b97 | |||
| 5b90f9fb87 | |||
| 9131dbf09b | |||
| a425234365 | |||
| cd4e268c72 | |||
| b94945a461 | |||
| 5b0a779c07 | |||
| d099bf2983 | |||
| f7add0e4dd | |||
| 1e1cc897ad | |||
| 04e2c59aa9 | |||
| 03bd3517be | |||
| eeba5bc8fd | |||
| 1f54180c9c | |||
| 8497a27c81 | |||
| 80afa470ee | |||
| 10c750e1a2 | |||
| 3aa175b36d | |||
| e76036965b | |||
| 2bb86a2ec1 | |||
| a89c8c6e5b | |||
| 1bdf205ca6 | |||
| ccf801137a | |||
| ef746a173e | |||
| e3552f4e36 | |||
| 75d9357488 | |||
| 26c028cf37 | |||
| 10c483967f | |||
| 07b33e8189 | |||
| fc3ea78005 | |||
| bd4150af5a | |||
| 1c6c93da35 | |||
| 0daec32ddd | |||
| e466ed9e06 | |||
| 4445b0317f | |||
| beaea6c3bf | |||
| a279c44aa5 | |||
| f1f96e574c | |||
| 1879001ef3 | |||
| 5014b1c459 | |||
| 903459cf7e | |||
| 7c04661cfa | |||
| 76d3194c96 | |||
| b63aee2851 | |||
| f57d2608c5 | |||
| 34331525b1 | |||
| 8d1f4427ae | |||
| 7f89f8284f | |||
| 3b2ac2bc3c | |||
| 73e88be8f3 | |||
| f5d092f497 | |||
| 8edbad92cb | |||
| b0e13a1352 | |||
| 5c75648cd7 | |||
| 0b1b99697e | |||
| 07202a8061 | |||
| 189454096b | |||
| 55ee5df852 | |||
| f6466b9154 | |||
| 8e5a64dca9 | |||
| b894147514 | |||
| d55c2c269f | |||
| c7afcf0bef | |||
| c79f5b8b74 | |||
| dc6b45cbcb | |||
| 73bbe5062d | |||
| 11eaf7ce7b | |||
| 590e97a99f | |||
| 4c09f3a229 | |||
| 260ca085bb | |||
| 7b00074b9e | |||
| 21d030db26 | |||
| 444b58504c | |||
| c1e2f4ad75 | |||
| ec6778e770 | |||
| 10e6c93a95 | |||
| 0060207816 | |||
| 2f6420e05f | |||
| c01699b6e4 | |||
| 6c6199add8 | |||
| 42cd5e0017 | |||
| baee6a37ea | |||
| 90b3685808 | |||
| e9a628066f | |||
| 8f46b5254e | |||
| a15e9d737c | |||
| 08f6f5cf71 | |||
| 4f9ce07304 | |||
| 743c598b83 | |||
| f7717571ea | |||
| fe84d35ce4 | |||
| 5c5414c960 | |||
| 2eeb1f588a | |||
| 9f69eacf71 | |||
| b2fda13ac3 | |||
| 88d34a5b92 | |||
| b91dd15f96 | |||
| 6e8e72f281 | |||
| 1311ce48d3 | |||
| 4a0dbba3ec | |||
| fcd0360135 | |||
| a94ba474f3 | |||
| a81ab0ea0f | |||
| 5417bf3445 | |||
| 8113ddb2d5 | |||
| 95c7d5eeff | |||
| fff89ee94c | |||
| 2745552915 | |||
| 2f400a2b17 | |||
| de5256b8f5 | |||
| 344ae053cf | |||
| d6b48a2818 | |||
| f8c7eee17b | |||
| c8d2118c74 | |||
| ddaa75a6dd | |||
| 182e06173d | |||
| f6b4600f8a | |||
| 6eaea0b73f | |||
| ee7dddf836 | |||
| e2e4743994 | |||
| 7b1a5aea19 | |||
| 602e13dab7 | |||
| 64117b92b0 | |||
| 17c89a3f2b | |||
| 9d33a2c524 | |||
| a470ba76df | |||
| 3ce017fa68 | |||
| d446da2243 | |||
| cdb4416421 | |||
| a1de8aa20c | |||
| adfad90f7c | |||
| 22703806cd | |||
| 22a63fc2ee | |||
| 7cbc0fa671 | |||
| 15346eec22 | |||
| c48e0c7377 | |||
| 95fdfa6396 | |||
| f8ecbf8f0b | |||
| c5ffbf6ed9 | |||
| 2115de9926 | |||
| 53149666ad | |||
| 5d55c5021b | |||
| 778da511a5 | |||
| 84940f12c5 | |||
| 0f45ecc00f | |||
| fc4ac029b1 | |||
| 73f4afa05e | |||
| ef1a42fff2 | |||
| 760d089073 | |||
| 92fbf83bdb | |||
| 9525bbf502 | |||
| 61df3bc462 |
@@ -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.
|
||||
|
||||
@@ -28,6 +28,7 @@ jobs:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
- "8.5"
|
||||
|
||||
name: PHP ${{ matrix.php-version }}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ jobs:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
- "8.5"
|
||||
|
||||
|
||||
name: PHP ${{ matrix.php-version }}
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-version:
|
||||
- "8.3"
|
||||
- "8.5"
|
||||
|
||||
name: PHP ${{ matrix.php-version }}
|
||||
|
||||
|
||||
+15
-4
@@ -10,9 +10,9 @@ however there are times when library dependencies and/or PHP/MySQL dependencies
|
||||
make it impossible to backport security fixes on older versions.
|
||||
|
||||
| Version | Supported |
|
||||
|---------| ------------------ |
|
||||
|---------|--------------------|
|
||||
| 8.x | :white_check_mark: |
|
||||
| 7.x | :white_check_mark: |
|
||||
| 7.x | :x: |
|
||||
| 6.x | :x: |
|
||||
| 5.1.x | :x: |
|
||||
| 5.0.x | :x: |
|
||||
@@ -24,7 +24,18 @@ make it impossible to backport security fixes on older versions.
|
||||
Security vulnerabilities should be sent to security@snipeitapp.com. You can typically expect a
|
||||
response within two business days, and we typically have fixes out in under a week from the initial disclosure.
|
||||
|
||||
This obviously varies based on the severity of the security issue and the difficulty in remediation,
|
||||
but those have historically been the timelines we worm around.
|
||||
This obviously varies based on the severity of the security issue and the difficulty in remediation, but those have
|
||||
historically been the timelines we work around.
|
||||
|
||||
We do ask that you do not disclose the vulnerability publicly until we have had a chance to address it and tag a release
|
||||
so that we can protect our users, and we will work
|
||||
with you to coordinate a public disclosure once we have a fix out. We will also work with you to ensure that you receive
|
||||
appropriate credit for the discovery of the vulnerability, if you would like to be credited. (Please provide a GitHub
|
||||
username or other information if you would like to be credited, and please let us know if you would like to remain
|
||||
anonymous.)
|
||||
|
||||
For responsible disclosure, we ask that you give us at least __90 days__ to address the issue before disclosing it
|
||||
publicly,
|
||||
but we will work with you if you need to disclose it sooner than that.
|
||||
|
||||
For a full breakdown of our security policies, please see https://snipeitapp.com/security.
|
||||
|
||||
@@ -19,7 +19,7 @@ class LdapSync extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'snipeit:ldap-sync {--location=} {--location_id=*} {--base_dn=} {--filter=} {--summary} {--json_summary}';
|
||||
protected $signature = 'snipeit:ldap-sync {--location=} {--location_id=*} {--base_dn=} {--filter=} {--delete} {--summary} {--json_summary}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -94,6 +94,7 @@ class LdapSync extends Command
|
||||
}
|
||||
|
||||
$summary = [];
|
||||
$seen_ldap_usernames = [];
|
||||
|
||||
try {
|
||||
|
||||
@@ -274,8 +275,14 @@ class LdapSync extends Command
|
||||
'name' => $item['department'],
|
||||
]);
|
||||
|
||||
$user = User::where('username', $item['username'])->first();
|
||||
$user = User::withTrashed()->where('username', $item['username'])->first();
|
||||
if (! empty($item['username'])) {
|
||||
$seen_ldap_usernames[] = $item['username'];
|
||||
}
|
||||
if ($user) {
|
||||
if ($user->trashed()) {
|
||||
$user->restore();
|
||||
}
|
||||
// Updating an existing user.
|
||||
$item['createorupdate'] = 'updated';
|
||||
} else {
|
||||
@@ -490,6 +497,41 @@ class LdapSync extends Command
|
||||
array_push($summary, $item);
|
||||
}
|
||||
|
||||
// Optionally soft-delete LDAP-imported users that are no longer present in LDAP.
|
||||
// users with assests etc. are not deletable and skipped
|
||||
if ($this->option('delete')) {
|
||||
$missing_ldap_users = User::where('ldap_import', 1);
|
||||
$missing_ldap_users = $missing_ldap_users->whereNotIn('username', $seen_ldap_usernames);
|
||||
$missing_ldap_users = $missing_ldap_users->get();
|
||||
|
||||
foreach ($missing_ldap_users as $missing_user) {
|
||||
$is_deletable = $this->isUserDeletable($missing_user);
|
||||
|
||||
$missing_item = [
|
||||
'id' => $missing_user->id,
|
||||
'username' => $missing_user->username,
|
||||
'firstname' => $missing_user->first_name,
|
||||
'lastname' => $missing_user->last_name,
|
||||
'email' => $missing_user->email,
|
||||
'createorupdate' => 'skipped',
|
||||
'status' => 'info',
|
||||
'deletable' => $is_deletable,
|
||||
'note' => $is_deletable ? 'missing from LDAP' : 'missing from LDAP, but not deletable',
|
||||
];
|
||||
|
||||
if ($is_deletable) {
|
||||
$missing_user->delete();
|
||||
$missing_item['createorupdate'] = 'deleted';
|
||||
$missing_item['status'] = 'success';
|
||||
$missing_item['note'] = 'deleted_missing_from_ldap';
|
||||
}
|
||||
|
||||
$summary[] = $missing_item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if ($this->option('summary')) {
|
||||
for ($x = 0; $x < count($summary); $x++) {
|
||||
if ($summary[$x]['status'] == 'error') {
|
||||
@@ -505,4 +547,23 @@ class LdapSync extends Command
|
||||
return $summary;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is deletable without gate check
|
||||
*
|
||||
* A user is considered deletable if they have no associated assets, accessories, licenses, consumables, managed users, or managed locations.
|
||||
*
|
||||
* @param User $user The user to check
|
||||
*
|
||||
* @return bool True if the user is deletable, false otherwise
|
||||
*/
|
||||
private function isUserDeletable(User $user): bool
|
||||
{
|
||||
return (($user->assets_count ?? $user->assets()->count()) === 0)
|
||||
&& (($user->accessories_count ?? $user->accessories()->count()) === 0)
|
||||
&& (($user->licenses_count ?? $user->licenses()->count()) === 0)
|
||||
&& (($user->consumables_count ?? $user->consumables()->count()) === 0)
|
||||
&& (($user->manages_users_count ?? $user->managesUsers()->count()) === 0)
|
||||
&& (($user->manages_locations_count ?? $user->managedLocations()->count()) === 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ class PurgeEulaPDFs extends Command
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'snipeit:purge-eula-pdfs
|
||||
{--older-than-days= : The number of days we should delete before }
|
||||
{--older-than-days= : The number of days we should delete before }
|
||||
{--company-id= : Only purge acceptances for users in this company}
|
||||
{--only-deleted-users : Only purge acceptances for deleted users, including soft-deleted or missing users}
|
||||
{--force : Skip the interactive yes/no prompt for confirmation}
|
||||
{--dryrun : Show the records that would be deleted but don\'t update the database or delete files from disk}
|
||||
{--with-output : Display the results in a table in your console}';
|
||||
@@ -55,7 +57,34 @@ class PurgeEulaPDFs extends Command
|
||||
$this->info('This script is being run with the --dryrun option. No files or records will be deleted.');
|
||||
|
||||
}
|
||||
$acceptances = CheckoutAcceptance::HasFiles()->where('updated_at', '<', $interval_date)->with('assignedTo')->get();
|
||||
$companyId = $this->option('company-id');
|
||||
$query = CheckoutAcceptance::HasFiles()->where('updated_at', '<', $interval_date)
|
||||
->with([
|
||||
'assignedTo' => function ($query) {
|
||||
$query->withTrashed();
|
||||
},
|
||||
]);
|
||||
|
||||
if ($this->option('only-deleted-users')) {
|
||||
$query->where(function ($query) use ($companyId) {
|
||||
$query->whereHas('assignedTo', function ($q) use ($companyId) {
|
||||
$q->withTrashed()->whereNotNull('deleted_at');
|
||||
|
||||
if ($companyId) {
|
||||
$q->where('company_id', $companyId);
|
||||
}
|
||||
});
|
||||
|
||||
$query->orWhereDoesntHave('assignedTo');
|
||||
});
|
||||
} else {
|
||||
if ($companyId) {
|
||||
$query->whereHas('assignedTo', function ($query) use ($companyId) {
|
||||
$query->withTrashed()->where('company_id', $companyId);
|
||||
});
|
||||
}
|
||||
}
|
||||
$acceptances = $query->get();
|
||||
|
||||
if (! $this->option('force')) {
|
||||
if ($this->confirm("\n****************************************************\nTHIS WILL DELETE ALL OF THE SIGNATURES AND EULA PDF FILES SINCE $interval_date. \nThere is NO undo! \n****************************************************\n\nDo you wish to continue? No backsies! [y|N]")) {
|
||||
|
||||
@@ -456,7 +456,11 @@ class RestoreFromBackup extends Command
|
||||
if (! file_exists($mysql_binary)) {
|
||||
return $this->error("mysql tool at: '$mysql_binary' does not exist, cannot restore. Please edit DB_DUMP_PATH in your .env to point to a directory that contains the mysqldump and mysql binary");
|
||||
}
|
||||
$proc_results = proc_open("$mysql_binary -h ".escapeshellarg(config('database.connections.mysql.host')).' -u '.escapeshellarg(config('database.connections.mysql.username')).' '.escapeshellarg(config('database.connections.mysql.database')), // yanked -p since we pass via ENV
|
||||
$proc_results = proc_open("$mysql_binary -h " .
|
||||
escapeshellarg(config('database.connections.mysql.host')) .
|
||||
' -u ' . escapeshellarg(config('database.connections.mysql.username')) . ' ' .
|
||||
' -P ' . escapeshellarg(config('database.connections.mysql.port')) . ' ' .
|
||||
escapeshellarg(config('database.connections.mysql.database')), // yanked -p since we pass via ENV
|
||||
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
|
||||
$pipes,
|
||||
null,
|
||||
|
||||
@@ -208,6 +208,30 @@ class AcceptanceController extends Controller
|
||||
'qty' => $acceptance->qty ?? 1,
|
||||
];
|
||||
|
||||
// Include asset custom fields that are explicitly allowed in outbound emails/PDFs.
|
||||
if ($item instanceof Asset && $item->model && $item->model->fieldset) {
|
||||
$customFields = [];
|
||||
$fields = $item->model->fieldset->fields
|
||||
->where('show_in_email', true)
|
||||
->where('field_encrypted', false);
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$dbColumn = $field->db_column;
|
||||
$value = $item->{$dbColumn};
|
||||
|
||||
if (! is_null($value) && $value !== '') {
|
||||
$customFields[] = [
|
||||
'label' => $field->name,
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($customFields)) {
|
||||
$data['custom_fields'] = $customFields;
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->input('asset_acceptance') === 'accepted') {
|
||||
|
||||
$pdf_filename = 'accepted-'.$acceptance->checkoutable_id.'-'.$acceptance->display_checkoutable_type.'-eula-'.date('Y-m-d-h-i-s').'.pdf';
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Http\Transformers\ActionlogsTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\AccessoryCheckout;
|
||||
use App\Models\Company;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -84,23 +85,23 @@ class AccessoriesController extends Controller
|
||||
}
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$accessories->where('category_id', '=', $request->input('category_id'));
|
||||
$accessories->where('accessories.category_id', '=', $request->input('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('manufacturer_id')) {
|
||||
$accessories->where('manufacturer_id', '=', $request->input('manufacturer_id'));
|
||||
$accessories->where('accessories.manufacturer_id', '=', $request->input('manufacturer_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('supplier_id')) {
|
||||
$accessories->where('supplier_id', '=', $request->input('supplier_id'));
|
||||
$accessories->where('accessories.supplier_id', '=', $request->input('supplier_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$accessories->where('location_id', '=', $request->input('location_id'));
|
||||
$accessories->where('accessories.location_id', '=', $request->input('location_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('notes')) {
|
||||
$accessories->where('notes', '=', $request->input('notes'));
|
||||
$accessories->where('accessories.notes', '=', $request->input('notes'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
@@ -155,6 +156,7 @@ class AccessoriesController extends Controller
|
||||
{
|
||||
$accessory = new Accessory;
|
||||
$accessory->fill($request->all());
|
||||
$accessory->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||
$accessory = $request->handleImages($accessory);
|
||||
|
||||
if ($accessory->save()) {
|
||||
@@ -248,6 +250,7 @@ class AccessoriesController extends Controller
|
||||
$this->authorize('update', Accessory::class);
|
||||
$accessory = Accessory::findOrFail($id);
|
||||
$accessory->fill($request->all());
|
||||
$accessory->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||
$accessory = $request->handleImages($accessory);
|
||||
|
||||
if ($accessory->save()) {
|
||||
|
||||
@@ -1067,6 +1067,12 @@ class AssetsController extends Controller
|
||||
});
|
||||
|
||||
if ($asset->save()) {
|
||||
|
||||
// Update the location of any child assets
|
||||
Asset::where('assigned_type', Asset::class)
|
||||
->where('assigned_to', $asset->id)
|
||||
->update(['location_id' => $asset->location_id]);
|
||||
|
||||
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues));
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', [
|
||||
@@ -1444,7 +1450,7 @@ class AssetsController extends Controller
|
||||
$label = new Label;
|
||||
|
||||
if (! $label) {
|
||||
throw new \Exception('Label object could not be created');
|
||||
throw new \Exception(trans('admin/labels/message.label_not_created'));
|
||||
}
|
||||
|
||||
// Configure label with assets and settings
|
||||
@@ -1465,7 +1471,7 @@ class AssetsController extends Controller
|
||||
|
||||
// Verify PDF was generated successfully
|
||||
if (empty($pdf_content)) {
|
||||
throw new \Exception('PDF content is empty');
|
||||
throw new \Exception(trans('admin/labels/message.use_new_label_engine_for_api'));
|
||||
}
|
||||
|
||||
$encoded_content = base64_encode($pdf_content);
|
||||
|
||||
@@ -9,8 +9,8 @@ use App\Http\Requests\ImageUploadRequest;
|
||||
use App\Http\Transformers\ActionlogsTransformer;
|
||||
use App\Http\Transformers\ComponentsTransformer;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Company;
|
||||
use App\Models\Component;
|
||||
use App\Models\ComponentAssignment;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -80,7 +80,7 @@ class ComponentsController extends Controller
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$components->where('name', '=', $request->input('name'));
|
||||
$components->where('components.name', '=', $request->input('name'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
@@ -92,27 +92,27 @@ class ComponentsController extends Controller
|
||||
}
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$components->where('category_id', '=', $request->input('category_id'));
|
||||
$components->where('components.category_id', '=', $request->input('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('supplier_id')) {
|
||||
$components->where('supplier_id', '=', $request->input('supplier_id'));
|
||||
$components->where('components.supplier_id', '=', $request->input('supplier_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('manufacturer_id')) {
|
||||
$components->where('manufacturer_id', '=', $request->input('manufacturer_id'));
|
||||
$components->where('components.manufacturer_id', '=', $request->input('manufacturer_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('model_number')) {
|
||||
$components->where('model_number', '=', $request->input('model_number'));
|
||||
$components->where('components.model_number', '=', $request->input('model_number'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$components->where('location_id', '=', $request->input('location_id'));
|
||||
$components->where('components.location_id', '=', $request->input('location_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('notes')) {
|
||||
$components->where('notes', '=', $request->input('notes'));
|
||||
$components->where('components.notes', '=', $request->input('notes'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
@@ -166,6 +166,7 @@ class ComponentsController extends Controller
|
||||
$this->authorize('create', Component::class);
|
||||
$component = new Component;
|
||||
$component->fill($request->all());
|
||||
$component->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||
$component = $request->handleImages($component);
|
||||
|
||||
if ($component->save()) {
|
||||
@@ -206,6 +207,7 @@ class ComponentsController extends Controller
|
||||
$this->authorize('update', Component::class);
|
||||
$component = Component::findOrFail($id);
|
||||
$component->fill($request->all());
|
||||
$component->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||
$component = $request->handleImages($component);
|
||||
|
||||
if ($component->save()) {
|
||||
@@ -252,13 +254,11 @@ class ComponentsController extends Controller
|
||||
{
|
||||
$this->authorize('view', Asset::class);
|
||||
|
||||
$component_checkouts = ComponentAssignment::where('component_id', $component->id)->with('adminuser')->with('assets');
|
||||
|
||||
$offset = request('offset', 0);
|
||||
$limit = $request->input('limit', 50);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$assets = $component_checkouts->assets()
|
||||
$assets = $component->assets()
|
||||
->where(function ($query) use ($request) {
|
||||
$search_str = '%'.$request->input('search').'%';
|
||||
$query->where('name', 'like', $search_str)
|
||||
|
||||
@@ -67,7 +67,7 @@ class ConsumablesController extends Controller
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$consumables->where('name', '=', $request->input('name'));
|
||||
$consumables->where('consumables.name', '=', $request->input('name'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
@@ -79,27 +79,27 @@ class ConsumablesController extends Controller
|
||||
}
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$consumables->where('category_id', '=', $request->input('category_id'));
|
||||
$consumables->where('consumables.category_id', '=', $request->input('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('model_number')) {
|
||||
$consumables->where('model_number', '=', $request->input('model_number'));
|
||||
$consumables->where('consumables.model_number', '=', $request->input('model_number'));
|
||||
}
|
||||
|
||||
if ($request->filled('manufacturer_id')) {
|
||||
$consumables->where('manufacturer_id', '=', $request->input('manufacturer_id'));
|
||||
$consumables->where('consumables.manufacturer_id', '=', $request->input('manufacturer_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('supplier_id')) {
|
||||
$consumables->where('supplier_id', '=', $request->input('supplier_id'));
|
||||
$consumables->where('consumables.supplier_id', '=', $request->input('supplier_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$consumables->where('location_id', '=', $request->input('location_id'));
|
||||
$consumables->where('consumables.location_id', '=', $request->input('location_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('notes')) {
|
||||
$consumables->where('notes', '=', $request->input('notes'));
|
||||
$consumables->where('consumables.notes', '=', $request->input('notes'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
@@ -155,6 +155,7 @@ class ConsumablesController extends Controller
|
||||
$this->authorize('create', Consumable::class);
|
||||
$consumable = new Consumable;
|
||||
$consumable->fill($request->all());
|
||||
$consumable->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||
$consumable = $request->handleImages($consumable);
|
||||
|
||||
if ($consumable->save()) {
|
||||
@@ -194,6 +195,7 @@ class ConsumablesController extends Controller
|
||||
$this->authorize('update', Consumable::class);
|
||||
$consumable = Consumable::findOrFail($id);
|
||||
$consumable->fill($request->all());
|
||||
$consumable->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||
$consumable = $request->handleImages($consumable);
|
||||
|
||||
if ($consumable->save()) {
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Http\Requests\ImageUploadRequest;
|
||||
use App\Http\Requests\StoreDepartmentRequest;
|
||||
use App\Http\Transformers\DepartmentsTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Models\Company;
|
||||
use App\Models\Department;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -50,23 +51,23 @@ class DepartmentsController extends Controller
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$departments->where('name', '=', $request->input('name'));
|
||||
$departments->where('departments.name', '=', $request->input('name'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$departments->where('company_id', '=', $request->input('company_id'));
|
||||
$departments->where('departments.company_id', '=', $request->input('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('manager_id')) {
|
||||
$departments->where('manager_id', '=', $request->input('manager_id'));
|
||||
$departments->where('departments.manager_id', '=', $request->input('manager_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$departments->where('location_id', '=', $request->input('location_id'));
|
||||
$departments->where('departments.location_id', '=', $request->input('location_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('tag_color')) {
|
||||
$departments->where('tag_color', '=', $request->input('departments.tag_color'));
|
||||
$departments->where('departments.tag_color', '=', $request->input('tag_color'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
@@ -111,6 +112,7 @@ class DepartmentsController extends Controller
|
||||
{
|
||||
$department = new Department;
|
||||
$department->fill($request->validated());
|
||||
$department->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||
$department = $request->handleImages($department);
|
||||
|
||||
$department->created_by = auth()->id();
|
||||
@@ -155,6 +157,7 @@ class DepartmentsController extends Controller
|
||||
$this->authorize('update', Department::class);
|
||||
$department = Department::findOrFail($id);
|
||||
$department->fill($request->all());
|
||||
$department->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||
$department = $request->handleImages($department);
|
||||
|
||||
if ($department->save()) {
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Http\Requests\FilterRequest;
|
||||
use App\Http\Transformers\ActionlogsTransformer;
|
||||
use App\Http\Transformers\LicensesTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Models\Company;
|
||||
use App\Models\License;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -179,6 +180,7 @@ class LicensesController extends Controller
|
||||
$this->authorize('create', License::class);
|
||||
$license = new License;
|
||||
$license->fill($request->all());
|
||||
$license->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||
|
||||
if ($license->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $license, trans('admin/licenses/message.create.success')));
|
||||
@@ -219,6 +221,7 @@ class LicensesController extends Controller
|
||||
|
||||
$license = License::findOrFail($id);
|
||||
$license->fill($request->all());
|
||||
$license->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||
|
||||
if ($license->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $license, trans('admin/licenses/message.update.success')));
|
||||
|
||||
@@ -257,9 +257,7 @@ class MaintenancesController extends Controller
|
||||
|
||||
public function history(Request $request, Maintenance $maintenance): JsonResponse|array
|
||||
{
|
||||
$this->authorize('view', Asset::class);
|
||||
$asset = $maintenance->asset;
|
||||
$this->authorize('history', $asset);
|
||||
$this->authorize('history', $maintenance);
|
||||
$historyQuery = $maintenance->getHistory($request);
|
||||
$total = (clone $historyQuery)->count();
|
||||
$offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
|
||||
|
||||
@@ -175,6 +175,10 @@ class AssetCheckinController extends Controller
|
||||
$asset->customFieldsForCheckinCheckout('display_checkin');
|
||||
|
||||
if ($asset->save()) {
|
||||
// Update the location of any child assets
|
||||
Asset::where('assigned_type', Asset::class)
|
||||
->where('assigned_to', $asset->id)
|
||||
->update(['location_id' => $asset->location_id]);
|
||||
|
||||
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues));
|
||||
|
||||
|
||||
@@ -66,7 +66,8 @@ class AssetsController extends Controller
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorize('index', Asset::class);
|
||||
$company = Company::find($request->input('company_id'));
|
||||
$companyId = $request->input('company_id');
|
||||
$company = is_scalar($companyId) ? Company::find($companyId) : null;
|
||||
|
||||
return view('hardware/index')->with('company', $company);
|
||||
}
|
||||
|
||||
@@ -106,15 +106,21 @@ class LoginController extends Controller
|
||||
if ($saml->isEnabled() && ! empty($samlData)) {
|
||||
|
||||
try {
|
||||
|
||||
$user = $saml->samlLogin($samlData);
|
||||
$notValidAfter = new \Carbon\Carbon(@$samlData['assertionNotOnOrAfter']);
|
||||
if (\Carbon::now()->greaterThanOrEqualTo($notValidAfter)) {
|
||||
abort(400, 'Expired SAML Assertion');
|
||||
}
|
||||
if (SamlNonce::where('nonce', @$samlData['nonce'])->count() > 0) {
|
||||
abort(400, 'Assertion has already been used');
|
||||
try {
|
||||
SamlNonce::create([
|
||||
'nonce' => $samlData['nonce'],
|
||||
'not_valid_after' => $notValidAfter,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
abort(400, 'Assertion has already been used.');
|
||||
}
|
||||
Log::debug('okay, fine, this is a new nonce then. Good for you.');
|
||||
if (! is_null($user)) {
|
||||
Auth::login($user);
|
||||
} else {
|
||||
@@ -128,10 +134,6 @@ class LoginController extends Controller
|
||||
$user->last_login = \Carbon::now();
|
||||
$user->saveQuietly();
|
||||
}
|
||||
$s = new SamlNonce;
|
||||
$s->nonce = @$samlData['nonce'];
|
||||
$s->not_valid_after = $notValidAfter;
|
||||
$s->save();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::debug('There was an error authenticating the SAML user: '.$e->getMessage());
|
||||
@@ -433,7 +435,7 @@ class LoginController extends Controller
|
||||
$user->saveQuietly();
|
||||
$request->session()->put('2fa_authed', $user->id);
|
||||
|
||||
return redirect()->route('home')->with('success', trans('auth/message.signin.success'));
|
||||
return redirect()->intended()->with('success', trans('auth/message.signin.success'));
|
||||
}
|
||||
|
||||
return redirect()->route('two-factor')->with('error', trans('auth/message.two_factor.invalid_code'));
|
||||
|
||||
@@ -74,8 +74,7 @@ class SamlController extends Controller
|
||||
public function login(Request $request)
|
||||
{
|
||||
$auth = $this->saml->getAuth();
|
||||
$ssoUrl = $auth->login(null, [], false, false, false, false);
|
||||
|
||||
$ssoUrl = $auth->login(session()->get('url.intended'), [], false, false, false, false);
|
||||
return redirect()->away($ssoUrl);
|
||||
}
|
||||
|
||||
@@ -96,6 +95,7 @@ class SamlController extends Controller
|
||||
$saml = $this->saml;
|
||||
$auth = $saml->getAuth();
|
||||
$saml_exception = false;
|
||||
session()->put('url.intended', $request->post('RelayState'));
|
||||
try {
|
||||
$auth->processResponse();
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\ImageUploadRequest;
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Maintenance;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
/**
|
||||
* This controller handles all actions related to Asset Maintenance for
|
||||
@@ -72,6 +75,7 @@ class MaintenancesController extends Controller
|
||||
public function store(ImageUploadRequest $request): RedirectResponse
|
||||
{
|
||||
$this->authorize('update', Asset::class);
|
||||
$this->validateUploadedFiles($request);
|
||||
|
||||
$assets = Asset::whereIn('id', $request->input('selected_assets'))->get();
|
||||
|
||||
@@ -102,12 +106,14 @@ class MaintenancesController extends Controller
|
||||
$maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
|
||||
}
|
||||
|
||||
$maintenance = $request->handleImages($maintenance);
|
||||
$request->handleImages($maintenance);
|
||||
|
||||
// Was the asset maintenance created?
|
||||
if (! $maintenance->save()) {
|
||||
return redirect()->back()->withInput()->withErrors($maintenance->getErrors());
|
||||
}
|
||||
|
||||
$this->storeUploadedFiles($request, $maintenance);
|
||||
}
|
||||
|
||||
return redirect()->route('maintenances.index')
|
||||
@@ -156,6 +162,7 @@ class MaintenancesController extends Controller
|
||||
{
|
||||
$this->authorize('update', Asset::class);
|
||||
$this->authorize('update', $maintenance->asset);
|
||||
$this->validateUploadedFiles($request);
|
||||
|
||||
$maintenance->supplier_id = $request->input('supplier_id');
|
||||
$maintenance->is_warranty = $request->input('is_warranty', 0);
|
||||
@@ -184,9 +191,11 @@ class MaintenancesController extends Controller
|
||||
$completionDate = Carbon::parse($maintenance->completion_date);
|
||||
$maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
|
||||
}
|
||||
$maintenance = $request->handleImages($maintenance);
|
||||
$request->handleImages($maintenance);
|
||||
|
||||
if ($maintenance->save()) {
|
||||
$this->storeUploadedFiles($request, $maintenance);
|
||||
|
||||
return redirect()->route('maintenances.index')
|
||||
->with('success', trans('admin/maintenances/message.edit.success'));
|
||||
}
|
||||
@@ -194,6 +203,56 @@ class MaintenancesController extends Controller
|
||||
return redirect()->back()->withInput()->withErrors($maintenance->getErrors());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores any generic file uploads submitted from the maintenance form.
|
||||
*/
|
||||
private function storeUploadedFiles(ImageUploadRequest $request, Maintenance $maintenance): void
|
||||
{
|
||||
if (! $request->hasFile('file')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$objectType = 'maintenances';
|
||||
$storagePath = self::$map_storage_path[$objectType];
|
||||
|
||||
if (! Storage::exists($storagePath)) {
|
||||
Storage::makeDirectory($storagePath, 775);
|
||||
}
|
||||
|
||||
$uploadFileRequest = app(UploadFileRequest::class);
|
||||
|
||||
foreach ((array) $request->file('file') as $file) {
|
||||
if (! $file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fileName = $uploadFileRequest->handleFile(
|
||||
$storagePath,
|
||||
self::$map_file_prefix[$objectType].'-'.$maintenance->id,
|
||||
$file
|
||||
);
|
||||
|
||||
$maintenance->logUpload($fileName, $request->input('file_notes'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate generic file uploads with the shared UploadFileRequest rules.
|
||||
*/
|
||||
private function validateUploadedFiles(ImageUploadRequest $request): void
|
||||
{
|
||||
if (! $request->hasFile('file')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uploadFileRequest = app(UploadFileRequest::class);
|
||||
|
||||
Validator::make(
|
||||
array_merge($request->all(), ['file' => $request->file('file')]),
|
||||
$uploadFileRequest->rules()
|
||||
)->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an asset maintenance
|
||||
*
|
||||
|
||||
@@ -798,6 +798,14 @@ class ReportsController extends Controller
|
||||
$assets->onlyTrashed();
|
||||
}
|
||||
|
||||
if ($request->input('assignment_status') === 'assigned') {
|
||||
$assets->whereNotNull('assets.assigned_to');
|
||||
}
|
||||
|
||||
if ($request->input('assignment_status') === 'unassigned') {
|
||||
$assets->whereNull('assets.assigned_to');
|
||||
}
|
||||
|
||||
$assets->orderBy('assets.id', 'ASC')->chunk(500, function ($assets) use ($handle, $customfields, $request) {
|
||||
|
||||
$executionTime = microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'];
|
||||
@@ -918,7 +926,7 @@ class ReportsController extends Controller
|
||||
|
||||
if ($request->filled('user_company')) {
|
||||
if ($asset->checkedOutToUser()) {
|
||||
$row[] = ($asset->assignedto?->company) ? $asset->assignedto->company->display_name : '';
|
||||
$row[] = ($asset->assignedto?->company) ? $asset->assignedto?->company?->display_name : '';
|
||||
} else {
|
||||
$row[] = ''; // Empty string if unassigned
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ class CheckForTwoFactor
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
redirect()->setIntendedUrl(url()->full()); // save the 'current' URL so we can send the user back to it?
|
||||
// Otherwise make sure they're enrolled and show them the 2FA code screen
|
||||
if ((auth()->user()->two_factor_secret != '') && (auth()->user()->two_factor_enrolled == '1')) {
|
||||
return redirect()->route('two-factor')->with('info', trans('auth/message.two_factor.enter_two_factor_code'));
|
||||
|
||||
@@ -29,6 +29,7 @@ class CustomAssetReportRequest extends Request
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'assignment_status' => 'nullable|in:all,assigned,unassigned',
|
||||
'purchase_start' => 'date|date_format:Y-m-d|nullable',
|
||||
'purchase_end' => 'date|date_format:Y-m-d|nullable',
|
||||
'purchase_cost_end' => 'numeric|nullable|gte:purchase_cost_start',
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Helpers\Helper;
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Models\Actionlog;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
@@ -24,6 +25,17 @@ class UploadedFilesTransformer
|
||||
public function transformFile(Actionlog $file)
|
||||
{
|
||||
$snipeModel = $file->item_type;
|
||||
$item = null;
|
||||
|
||||
if (is_string($snipeModel) && class_exists($snipeModel)) {
|
||||
$itemQuery = $snipeModel::query();
|
||||
|
||||
if (in_array(SoftDeletes::class, class_uses_recursive($snipeModel), true)) {
|
||||
$itemQuery->withTrashed();
|
||||
}
|
||||
|
||||
$item = $itemQuery->find($file->item_id);
|
||||
}
|
||||
|
||||
$array = [
|
||||
'id' => (int) $file->id,
|
||||
@@ -49,7 +61,7 @@ class UploadedFilesTransformer
|
||||
];
|
||||
|
||||
$permissions_array['available_actions'] = [
|
||||
'delete' => (Gate::allows('update', $snipeModel) && ($file->deleted_at == '')),
|
||||
'delete' => (Gate::allows('update', $item ?? $snipeModel) && ($file->deleted_at == '')),
|
||||
];
|
||||
|
||||
$array += $permissions_array;
|
||||
|
||||
@@ -34,6 +34,7 @@ use App\Notifications\CheckoutLicenseSeatNotification;
|
||||
use Exception;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Notification as BaseNotification;
|
||||
use Illuminate\Support\Facades\Context;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
@@ -126,12 +127,12 @@ class CheckoutableListener
|
||||
if ($shouldSendWebhookNotification) {
|
||||
try {
|
||||
if ($this->newMicrosoftTeamsWebhookEnabled()) {
|
||||
$message = $this->getCheckoutNotification($event)->toMicrosoftTeams();
|
||||
$message = $this->getCheckoutNotification($event, $acceptance, true)->toMicrosoftTeams();
|
||||
$notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint);
|
||||
$notification->success()->sendMessage($message[0], $message[1]); // Send the message to Microsoft Teams
|
||||
} else {
|
||||
Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint)
|
||||
->notify($this->getCheckoutNotification($event, $acceptance));
|
||||
->notify($this->getCheckoutNotification($event, $acceptance, true));
|
||||
}
|
||||
} catch (ClientException $e) {
|
||||
$status = $e->getResponse()->getStatusCode();
|
||||
@@ -233,12 +234,12 @@ class CheckoutableListener
|
||||
// Send Webhook notification
|
||||
try {
|
||||
if ($this->newMicrosoftTeamsWebhookEnabled()) {
|
||||
$message = $this->getCheckinNotification($event)->toMicrosoftTeams();
|
||||
$message = $this->getCheckinNotification($event, true)->toMicrosoftTeams();
|
||||
$notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint);
|
||||
$notification->success()->sendMessage($message[0], $message[1]); // Send the message to Microsoft Teams
|
||||
} else {
|
||||
Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint)
|
||||
->notify($this->getCheckinNotification($event));
|
||||
->notify($this->getCheckinNotification($event, true));
|
||||
}
|
||||
} catch (ClientException $e) {
|
||||
$status = $e->getResponse()->getStatusCode();
|
||||
@@ -312,12 +313,12 @@ class CheckoutableListener
|
||||
* @param CheckoutableCheckedIn $event
|
||||
* @return Notification
|
||||
*/
|
||||
private function getCheckinNotification($event)
|
||||
private function getCheckinNotification($event, bool $refreshCheckoutable = false): BaseNotification
|
||||
{
|
||||
|
||||
$notificationClass = null;
|
||||
$checkoutable = $this->getCheckoutableForNotification($event->checkoutable, $refreshCheckoutable);
|
||||
|
||||
switch (get_class($event->checkoutable)) {
|
||||
switch (get_class($checkoutable)) {
|
||||
case Accessory::class:
|
||||
$notificationClass = CheckinAccessoryNotification::class;
|
||||
break;
|
||||
@@ -334,7 +335,7 @@ class CheckoutableListener
|
||||
|
||||
Log::debug('Notification class: '.$notificationClass);
|
||||
|
||||
return new $notificationClass($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note);
|
||||
return new $notificationClass($checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -344,11 +345,12 @@ class CheckoutableListener
|
||||
* @param CheckoutAcceptance|null $acceptance
|
||||
* @return Notification
|
||||
*/
|
||||
private function getCheckoutNotification($event, $acceptance = null)
|
||||
private function getCheckoutNotification($event, $acceptance = null, bool $refreshCheckoutable = false): BaseNotification
|
||||
{
|
||||
$notificationClass = null;
|
||||
$checkoutable = $this->getCheckoutableForNotification($event->checkoutable, $refreshCheckoutable);
|
||||
|
||||
switch (get_class($event->checkoutable)) {
|
||||
switch (get_class($checkoutable)) {
|
||||
case Accessory::class:
|
||||
$notificationClass = CheckoutAccessoryNotification::class;
|
||||
break;
|
||||
@@ -366,7 +368,16 @@ class CheckoutableListener
|
||||
break;
|
||||
}
|
||||
|
||||
return new $notificationClass($event->checkoutable, $event->checkedOutTo, $event->checkedOutBy, $acceptance, $event->note);
|
||||
return new $notificationClass($checkoutable, $event->checkedOutTo, $event->checkedOutBy, $acceptance, $event->note);
|
||||
}
|
||||
|
||||
private function getCheckoutableForNotification(Model $checkoutable, bool $shouldRefresh): Model
|
||||
{
|
||||
if (! $shouldRefresh) {
|
||||
return $checkoutable;
|
||||
}
|
||||
|
||||
return $checkoutable->fresh() ?? $checkoutable;
|
||||
}
|
||||
|
||||
private function getCheckoutMailType($event, $acceptance)
|
||||
|
||||
@@ -135,14 +135,18 @@ class CheckoutablesCheckedOutInBulkListener
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getNotifiableUser(CheckoutablesCheckedOutInBulk $event): ?Model
|
||||
private function getNotifiableUser(CheckoutablesCheckedOutInBulk $event): ?User
|
||||
{
|
||||
$target = $event->target;
|
||||
|
||||
if ($target instanceof Asset) {
|
||||
$target->load('assignedTo');
|
||||
|
||||
return $target->assignedto;
|
||||
if ($target->assigned instanceof User) {
|
||||
return $target->assigned;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($target instanceof Location) {
|
||||
|
||||
@@ -639,6 +639,10 @@ class Importer extends Component
|
||||
'color code',
|
||||
trans('general.tag_color'),
|
||||
],
|
||||
'checkout_class' => [
|
||||
'checkout type',
|
||||
'checkout class',
|
||||
],
|
||||
];
|
||||
|
||||
$this->columnOptions[''] = $this->getColumns(''); // blank mode? I don't know what this is supposed to mean
|
||||
|
||||
@@ -58,10 +58,26 @@ class CheckinAssetMail extends BaseMailable
|
||||
{
|
||||
$this->item->load('status');
|
||||
$fields = [];
|
||||
$customFields = [];
|
||||
|
||||
// Check if the item has custom fields associated with it
|
||||
if (($this->item->model) && ($this->item->model->fieldset)) {
|
||||
$fields = $this->item->model->fieldset->fields;
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (! $field->show_in_email || $field->field_encrypted == '1') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->item->{$field->db_column_name()};
|
||||
|
||||
if (! is_null($value) && $value !== '') {
|
||||
$customFields[] = [
|
||||
'label' => $field->name,
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Content(
|
||||
@@ -73,6 +89,7 @@ class CheckinAssetMail extends BaseMailable
|
||||
'note' => $this->note,
|
||||
'target' => $this->target,
|
||||
'fields' => $fields,
|
||||
'custom_fields' => $customFields,
|
||||
'expected_checkin' => $this->expected_checkin,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -75,6 +75,7 @@ class CheckoutAssetMail extends BaseMailable
|
||||
$eula = method_exists($this->item, 'getEula') ? $this->item->getEula() : '';
|
||||
$req_accept = $this->requiresAcceptance();
|
||||
$fields = [];
|
||||
$customFields = [];
|
||||
$name = null;
|
||||
|
||||
if ($this->target instanceof User) {
|
||||
@@ -88,6 +89,21 @@ class CheckoutAssetMail extends BaseMailable
|
||||
// Check if the item has custom fields associated with it
|
||||
if (($this->item->model) && ($this->item->model->fieldset)) {
|
||||
$fields = $this->item->model->fieldset->fields;
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (! $field->show_in_email || $field->field_encrypted == '1') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->item->{$field->db_column_name()};
|
||||
|
||||
if (! is_null($value) && $value !== '') {
|
||||
$customFields[] = [
|
||||
'label' => $field->name,
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$accept_url = is_null($this->acceptance) ? null : route('account.accept.item', $this->acceptance);
|
||||
@@ -101,6 +117,7 @@ class CheckoutAssetMail extends BaseMailable
|
||||
'note' => $this->note,
|
||||
'target' => $name,
|
||||
'fields' => $fields,
|
||||
'custom_fields' => $customFields,
|
||||
'eula' => $eula,
|
||||
'req_accept' => $req_accept,
|
||||
'accept_url' => $accept_url,
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user