Compare commits

...

45 Commits

Author SHA1 Message Date
snipe f697ef1d03 Pint 2026-05-09 12:20:45 +01:00
snipe 256003b675 Added password reset prompt 2026-05-09 12:20:40 +01:00
snipe 464db7f473 Last one (I hope) 2026-05-08 15:59:37 +01:00
snipe a56426e6f4 And still more 2026-05-08 15:59:00 +01:00
snipe 19e58a8640 Still more localization 2026-05-08 15:56:03 +01:00
snipe d83b64ff32 Added tests 2026-05-08 15:55:48 +01:00
snipe e839d989ec Still more localizations 2026-05-08 15:53:01 +01:00
snipe b8d2be6c3a Added test 2026-05-08 15:52:45 +01:00
snipe b264e07327 More localizations 2026-05-08 15:52:05 +01:00
snipe 25a08faa6d Updated readme 2026-05-08 15:51:45 +01:00
snipe 926afa6c28 Added throttle 2026-05-08 15:43:26 +01:00
snipe e3a042f334 More translations 2026-05-08 15:39:23 +01:00
snipe 082ebeb27f Localize prompts and tools 2026-05-08 15:39:09 +01:00
snipe aed11dfce7 Added readme 2026-05-08 15:18:54 +01:00
snipe 4090e05536 Pint 2026-05-08 15:18:46 +01:00
snipe 49818175cd Split name into two pieces 2026-05-08 15:17:55 +01:00
snipe ef4b2349eb Added common prompts 2026-05-08 15:14:27 +01:00
snipe 926f7dd5f7 Added profile update tool 2026-05-08 15:03:49 +01:00
snipe 8ccc705473 Add a tool to update your own profile 2026-05-08 14:58:08 +01:00
snipe c75d0effe2 Pint :( 2026-05-08 13:10:06 +01:00
snipe 96a3a11f00 This doesn’t actually work yet 2026-05-08 13:09:54 +01:00
snipe 9c97a06c7e Additional tools 2026-05-08 11:45:30 +01:00
snipe 2542221fc9 Added tests 2026-05-08 11:45:16 +01:00
snipe 664a1906c1 Dept tooling 2026-05-08 10:59:06 +01:00
snipe 08b2d0c85d Licenses MCP stuff 2026-05-08 10:37:25 +01:00
snipe dc9f0104f6 Gate checks and accessory scoping 2026-05-07 22:40:23 +01:00
snipe 6b2f2d68b7 Add/delete/checkout/checkin/edit MCP tools for Components 2026-05-07 17:45:29 +01:00
snipe 9aa5ba5cd0 MCP for accessories management 2026-05-07 17:38:03 +01:00
snipe b74e79b814 Added user create, show, list, delete 2026-05-07 17:27:20 +01:00
snipe 7636c2436c TEMPORARILY remove api auth from MCP routes - this is breaking the inspector for me 2026-05-07 16:36:39 +01:00
snipe 0eec6e3688 Fixed tests 2026-05-07 16:36:07 +01:00
snipe d961714358 Updated response 2026-05-07 16:34:38 +01:00
snipe 51bdc3b020 Added audit, delete and update tools 2026-05-07 16:23:34 +01:00
snipe 6a47b4e6a7 More tests 2026-05-07 16:23:08 +01:00
snipe 656dae04a7 Added views 2026-05-07 16:09:21 +01:00
snipe 2f3df9a085 Allow lookup by serial number 2026-05-07 16:01:46 +01:00
snipe 0514901cbc Updated docs for laravel 12 2026-05-07 16:01:27 +01:00
snipe cc0169d2f7 Use auth:api on routes 2026-05-07 16:01:13 +01:00
snipe 490ce6fa5d Added passport oauth for mcp 2026-05-07 16:00:46 +01:00
snipe b731ec6dd6 Added oauth routes to MCP 2026-05-07 15:58:36 +01:00
snipe 91bd2064fd Added tests 2026-05-07 15:54:37 +01:00
snipe deb56f250f Added routes file 2026-05-07 15:54:30 +01:00
snipe 7d57ce4679 Added basic asset tools 2026-05-07 15:54:23 +01:00
snipe 84fea96949 Added AssetBuilder to sequester scopes better
This isn’t fully baked yet - it would touch way too much main code to flip it over just yet
2026-05-07 15:41:01 +01:00
snipe eada5f503c Install laravel MCP 2026-05-07 14:41:02 +01:00
223 changed files with 22746 additions and 10 deletions
+4 -2
View File
@@ -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.
+45
View File
@@ -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),
];
}
}
+54
View File
@@ -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),
];
}
}
+64
View File
@@ -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),
];
}
}
+24
View File
@@ -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}.";
}
}
+44
View File
@@ -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, 3160 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
View File
File diff suppressed because it is too large Load Diff
+275
View File
@@ -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,
];
}
+97
View File
@@ -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'),
];
}
}
+120
View File
@@ -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'),
];
}
}
+82
View File
@@ -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'),
];
}
}
+110
View File
@@ -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'),
];
}
}
+102
View File
@@ -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)'),
];
}
}
+105
View File
@@ -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'),
];
}
}
+134
View File
@@ -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'),
];
}
}
+145
View File
@@ -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'),
];
}
}
+121
View File
@@ -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)'),
];
}
}
+113
View File
@@ -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'),
];
}
}
+149
View File
@@ -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'),
];
}
}
+107
View File
@@ -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'),
];
}
}
+97
View File
@@ -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'),
];
}
}
+108
View File
@@ -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'),
];
}
}
+89
View File
@@ -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'),
];
}
}
+90
View File
@@ -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'),
];
}
}
+107
View File
@@ -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'),
];
}
}
+106
View File
@@ -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'),
];
}
}
+87
View File
@@ -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'),
];
}
}
+74
View File
@@ -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'),
];
}
}
+130
View File
@@ -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'),
];
}
}
+119
View File
@@ -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'),
];
}
}
+97
View File
@@ -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'),
];
}
}
+105
View File
@@ -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'),
];
}
}
+83
View File
@@ -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'),
];
}
}
+95
View File
@@ -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'),
];
}
}
+96
View File
@@ -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'),
];
}
}
+155
View File
@@ -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'),
];
}
}
+83
View File
@@ -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'),
];
}
}
+83
View File
@@ -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'),
];
}
}
+94
View File
@@ -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'),
];
}
}
+83
View File
@@ -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'),
];
}
}
+79
View File
@@ -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'),
];
}
}
+83
View File
@@ -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'),
];
}
}
+83
View File
@@ -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'),
];
}
}
+83
View File
@@ -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'),
];
}
}
+79
View File
@@ -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'),
];
}
}
+75
View File
@@ -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'),
];
}
}
+89
View File
@@ -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'),
];
}
}
+87
View File
@@ -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'),
];
}
}
+79
View File
@@ -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'),
];
}
}
+83
View File
@@ -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'),
];
}
}
+79
View File
@@ -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'),
];
}
}
+94
View File
@@ -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'),
];
}
}
+102
View File
@@ -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(),
];
}
}
+72
View File
@@ -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'),
];
}
}
+82
View File
@@ -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'),
];
}
}
+101
View File
@@ -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(),
];
}
}
+116
View File
@@ -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'),
];
}
}
+141
View File
@@ -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(),
];
}
}
+98
View File
@@ -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(),
];
}
}
+88
View File
@@ -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(),
];
}
}
+110
View File
@@ -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(),
];
}
}
+82
View File
@@ -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(),
];
}
}
+81
View File
@@ -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(),
];
}
}
+152
View File
@@ -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'),
];
}
}
+113
View File
@@ -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(),
];
}
}
+101
View File
@@ -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(),
];
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Asset;
use App\Models\Maintenance;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('list_maintenances')]
#[Title('List Maintenances')]
#[Description('List asset maintenances with optional filtering by asset and pagination')]
class ListMaintenancesTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
if (! Gate::allows('view', Asset::class)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$request->validate([
'asset_id' => 'nullable|integer',
'limit' => 'nullable|integer|min:1|max:500',
'offset' => 'nullable|integer|min:0',
]);
$maintenances = Maintenance::with('asset', 'supplier');
if ($request->filled('asset_id')) {
$maintenances->where('asset_id', $request->get('asset_id'));
}
$maintenances->orderBy('created_at', 'desc');
$total = $maintenances->count();
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
$results = $maintenances->skip($offset)->take($limit)->get();
$maintenancesData = $results->map(fn (Maintenance $maintenance) => [
'id' => $maintenance->id,
'title' => $maintenance->name,
'asset_id' => $maintenance->asset_id,
'asset_tag' => $maintenance->asset?->asset_tag,
'is_warranty' => (bool) $maintenance->is_warranty,
'cost' => $maintenance->cost,
'start_date' => $maintenance->start_date,
'completion_date' => $maintenance->completion_date,
'supplier' => $maintenance->supplier?->name,
'notes' => $maintenance->notes,
])->values()->all();
return Response::make(
Response::text(trans('mcp.list_maintenances', ['total' => $total, 'count' => count($maintenancesData)]))
)->withStructuredContent([
'total' => $total,
'offset' => $offset,
'limit' => $limit,
'maintenances' => $maintenancesData,
]);
}
public function schema(JsonSchema $schema): array
{
return [
'asset_id' => $schema->number()->description('Filter by asset ID'),
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'total' => $schema->number()->description('Total number of matching maintenances')->required(),
'offset' => $schema->number()->description('Current pagination offset')->required(),
'limit' => $schema->number()->description('Results per page')->required(),
'maintenances' => $schema->array()->description('List of maintenances')->required(),
];
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Manufacturer;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('list_manufacturers')]
#[Title('List Manufacturers')]
#[Description('Search and list Snipe-IT manufacturers with optional filtering and pagination')]
class ListManufacturersTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
if (! Gate::allows('view', Manufacturer::class)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$request->validate([
'search' => 'nullable|string|max:255',
'limit' => 'nullable|integer|min:1|max:500',
'offset' => 'nullable|integer|min:0',
]);
$manufacturers = Manufacturer::withCount(
'assets as assets_count',
'licenses as licenses_count',
'accessories as accessories_count',
'components as components_count'
);
if ($request->filled('search')) {
$manufacturers->TextSearch($request->get('search'));
}
$manufacturers->orderBy('created_at', 'desc');
$total = $manufacturers->count();
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
$results = $manufacturers->skip($offset)->take($limit)->get();
$manufacturersData = $results->map(fn (Manufacturer $manufacturer) => [
'id' => $manufacturer->id,
'name' => $manufacturer->name,
'url' => $manufacturer->url,
'support_url' => $manufacturer->support_url,
'support_email' => $manufacturer->support_email,
'support_phone' => $manufacturer->support_phone,
'assets_count' => $manufacturer->assets_count,
'licenses_count' => $manufacturer->licenses_count,
'accessories_count' => $manufacturer->accessories_count,
'components_count' => $manufacturer->components_count,
])->values()->all();
return Response::make(
Response::text(trans('mcp.list_manufacturers', ['total' => $total, 'count' => count($manufacturersData)]))
)->withStructuredContent([
'total' => $total,
'offset' => $offset,
'limit' => $limit,
'manufacturers' => $manufacturersData,
]);
}
public function schema(JsonSchema $schema): array
{
return [
'search' => $schema->string()->description('Keyword to search across manufacturer name and notes'),
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'total' => $schema->number()->description('Total number of matching manufacturers')->required(),
'offset' => $schema->number()->description('Current pagination offset')->required(),
'limit' => $schema->number()->description('Results per page')->required(),
'manufacturers' => $schema->array()->description('List of manufacturers')->required(),
];
}
}
+99
View File
@@ -0,0 +1,99 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Statuslabel;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('list_status_labels')]
#[Title('List Status Labels')]
#[Description('Search and list Snipe-IT status labels with optional filtering and pagination')]
class ListStatusLabelsTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
if (! Gate::allows('view', Statuslabel::class)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$request->validate([
'search' => 'nullable|string|max:255',
'status_type' => 'nullable|string|in:deployable,pending,archived,undeployable',
'limit' => 'nullable|integer|min:1|max:500',
'offset' => 'nullable|integer|min:0',
]);
$labels = Statuslabel::withCount('assets as assets_count');
if ($request->filled('search')) {
$labels->TextSearch($request->get('search'));
}
if ($request->filled('status_type')) {
$type = $request->get('status_type');
if ($type === 'deployable') {
$labels->Deployable();
} elseif ($type === 'pending') {
$labels->Pending();
} elseif ($type === 'archived') {
$labels->Archived();
} elseif ($type === 'undeployable') {
$labels->Undeployable();
}
}
$total = $labels->count();
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
$results = $labels->skip($offset)->take($limit)->get();
$labelsData = $results->map(fn (Statuslabel $label) => [
'id' => $label->id,
'name' => $label->name,
'type' => $label->getStatuslabelType(),
'color' => $label->color,
'assets_count' => $label->assets_count,
'deployable' => $label->deployable,
'pending' => $label->pending,
'archived' => $label->archived,
])->values()->all();
return Response::make(
Response::text(trans('mcp.list_status_labels', ['total' => $total, 'count' => count($labelsData)]))
)->withStructuredContent([
'total' => $total,
'offset' => $offset,
'limit' => $limit,
'status_labels' => $labelsData,
]);
}
public function schema(JsonSchema $schema): array
{
return [
'search' => $schema->string()->description('Keyword to search across status label name and notes'),
'status_type' => $schema->string()->description('Filter by type: deployable, pending, archived, undeployable'),
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'total' => $schema->number()->description('Total number of matching status labels')->required(),
'offset' => $schema->number()->description('Current pagination offset')->required(),
'limit' => $schema->number()->description('Results per page')->required(),
'status_labels' => $schema->array()->description('List of status labels')->required(),
];
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Supplier;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('list_suppliers')]
#[Title('List Suppliers')]
#[Description('Search and list Snipe-IT suppliers with optional filtering and pagination')]
class ListSuppliersTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
if (! Gate::allows('view', Supplier::class)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$request->validate([
'search' => 'nullable|string|max:255',
'limit' => 'nullable|integer|min:1|max:500',
'offset' => 'nullable|integer|min:0',
]);
$suppliers = Supplier::withCount(
'assets as assets_count',
'licenses as licenses_count'
);
if ($request->filled('search')) {
$suppliers->TextSearch($request->get('search'));
}
$suppliers->orderBy('created_at', 'desc');
$total = $suppliers->count();
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
$results = $suppliers->skip($offset)->take($limit)->get();
$suppliersData = $results->map(fn (Supplier $supplier) => [
'id' => $supplier->id,
'name' => $supplier->name,
'address' => $supplier->address,
'city' => $supplier->city,
'state' => $supplier->state,
'country' => $supplier->country,
'phone' => $supplier->phone,
'email' => $supplier->email,
'url' => $supplier->url,
'assets_count' => $supplier->assets_count,
'licenses_count' => $supplier->licenses_count,
])->values()->all();
return Response::make(
Response::text(trans('mcp.list_suppliers', ['total' => $total, 'count' => count($suppliersData)]))
)->withStructuredContent([
'total' => $total,
'offset' => $offset,
'limit' => $limit,
'suppliers' => $suppliersData,
]);
}
public function schema(JsonSchema $schema): array
{
return [
'search' => $schema->string()->description('Keyword to search across supplier fields'),
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'total' => $schema->number()->description('Total number of matching suppliers')->required(),
'offset' => $schema->number()->description('Current pagination offset')->required(),
'limit' => $schema->number()->description('Results per page')->required(),
'suppliers' => $schema->array()->description('List of suppliers')->required(),
];
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Company;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\Department;
use App\Models\License;
use App\Models\Location;
use App\Models\Maintenance;
use App\Models\Supplier;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('list_uploads')]
#[Title('List Uploads')]
#[Description('List files uploaded to a Snipe-IT object. Supported types: accessories, assets, companies, components, consumables, departments, licenses, locations, maintenances, models, suppliers, users')]
class ListUploadsTool extends Tool
{
private const TYPE_MAP = [
'accessories' => Accessory::class,
'assets' => Asset::class,
'companies' => Company::class,
'components' => Component::class,
'consumables' => Consumable::class,
'departments' => Department::class,
'licenses' => License::class,
'locations' => Location::class,
'maintenances' => Maintenance::class,
'models' => AssetModel::class,
'suppliers' => Supplier::class,
'users' => User::class,
];
public function handle(Request $request): ResponseFactory
{
$validTypes = implode(',', array_keys(self::TYPE_MAP));
$request->validate([
'object_type' => 'required|string|in:'.$validTypes,
'id' => 'required|integer|min:1',
'limit' => 'nullable|integer|min:1|max:500',
'offset' => 'nullable|integer|min:0',
]);
$objectType = $request->get('object_type');
$modelClass = self::TYPE_MAP[$objectType];
$object = $modelClass::withTrashed()->find($request->get('id'));
if (! $object) {
return Response::make(Response::error(trans('mcp.object_not_found', ['type' => $objectType])));
}
if (! Gate::allows('files', $object)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
$query = $object->uploads()->with('adminuser');
$total = (clone $query)->count();
$uploads = $query->skip($offset)->take($limit)->orderBy('created_at', 'desc')->get();
$files = $uploads->map(fn ($file) => [
'id' => $file->id,
'filename' => $file->filename,
'url' => $file->uploads_file_url(),
'note' => $file->note,
'created_by' => $file->adminuser ? [
'id' => $file->adminuser->id,
'username' => $file->adminuser->username,
] : null,
'created_at' => $file->created_at?->toISOString(),
'exists_on_disk' => Storage::exists($file->uploads_file_path()),
])->values()->all();
return Response::make(
Response::text(trans('mcp.list_uploads', [
'total' => $total,
'count' => count($files),
'type' => $objectType,
]))
)->withStructuredContent([
'object_type' => $objectType,
'object_id' => $object->id,
'total' => $total,
'offset' => $offset,
'limit' => $limit,
'files' => $files,
]);
}
public function schema(JsonSchema $schema): array
{
return [
'object_type' => $schema->string()->description('Type of object: accessories, assets, companies, components, consumables, departments, licenses, locations, maintenances, models, suppliers, users'),
'id' => $schema->number()->description('Numeric ID of the object'),
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'object_type' => $schema->string()->description('Type of object queried')->required(),
'object_id' => $schema->number()->description('ID of the object queried')->required(),
'total' => $schema->number()->description('Total number of uploaded files')->required(),
'offset' => $schema->number()->description('Current pagination offset')->required(),
'limit' => $schema->number()->description('Results per page')->required(),
'files' => $schema->array()->description('List of uploaded files'),
];
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
namespace App\Mcp\Tools;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('list_users')]
#[Title('List Users')]
#[Description('Search and list Snipe-IT users with optional filtering by keyword, company, department, and location')]
class ListUsersTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
if (! Gate::allows('index', User::class)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$request->validate([
'search' => 'nullable|string|max:255',
'company_id' => 'nullable|integer',
'department_id' => 'nullable|integer',
'location_id' => 'nullable|integer',
'activated' => 'nullable|boolean',
'limit' => 'nullable|integer|min:1|max:500',
'offset' => 'nullable|integer|min:0',
]);
$users = User::with('company', 'department', 'userloc', 'manager')
->withCount(['assets as assets_count', 'licenses as licenses_count']);
if ($request->filled('search')) {
$users->TextSearch($request->get('search'));
}
if ($request->filled('company_id')) {
$users->where('users.company_id', '=', $request->get('company_id'));
}
if ($request->filled('department_id')) {
$users->where('users.department_id', '=', $request->get('department_id'));
}
if ($request->filled('location_id')) {
$users->where('users.location_id', '=', $request->get('location_id'));
}
if ($request->has('activated')) {
$users->where('users.activated', '=', $request->get('activated'));
}
$users->orderBy('users.last_name', 'asc')->orderBy('users.first_name', 'asc');
$total = $users->count();
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
$results = $users->skip($offset)->take($limit)->get();
$usersData = $results->map(fn (User $user) => [
'id' => $user->id,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'username' => $user->username,
'email' => $user->email,
'jobtitle' => $user->jobtitle,
'company' => $user->company?->name,
'department' => $user->department?->name,
'location' => $user->userloc?->name,
'manager' => $user->manager ? trim($user->manager->first_name.' '.$user->manager->last_name) : null,
'activated' => (bool) $user->activated,
'assets_count' => $user->assets_count,
'licenses_count' => $user->licenses_count,
])->values()->all();
return Response::make(
Response::text(trans('mcp.list_users', ['total' => $total, 'count' => count($usersData)]))
)->withStructuredContent([
'total' => $total,
'offset' => $offset,
'limit' => $limit,
'users' => $usersData,
]);
}
public function schema(JsonSchema $schema): array
{
return [
'search' => $schema->string()->description('Keyword to search across name, username, email, and employee number'),
'company_id' => $schema->number()->description('Filter by company ID'),
'department_id' => $schema->number()->description('Filter by department ID'),
'location_id' => $schema->number()->description('Filter by location ID'),
'activated' => $schema->boolean()->description('Filter by account activated status'),
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'total' => $schema->number()->description('Total number of matching users')->required(),
'offset' => $schema->number()->description('Current pagination offset')->required(),
'limit' => $schema->number()->description('Results per page')->required(),
];
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace App\Mcp\Tools;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('reset_2fa')]
#[Title('Reset Two-Factor Authentication')]
#[Description('Reset two-factor authentication for a Snipe-IT user')]
class Reset2FATool extends Tool
{
public function handle(Request $request): ResponseFactory
{
if (! Gate::allows('update', User::class)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$request->validate([
'id' => 'required|integer',
]);
$user = User::find($request->get('id'));
if (! $user) {
return Response::make(Response::error(trans('mcp.user_not_found')));
}
$user->two_factor_secret = null;
$user->two_factor_enrolled = 0;
$user->two_factor_optin = 0;
$user->save();
return Response::make(
Response::text(trans('mcp.two_factor_reset', ['username' => $user->username]))
)->withStructuredContent([
'success' => true,
'message' => trans('mcp.two_factor_reset', ['username' => $user->username]),
'id' => $user->id,
'username' => $user->username,
]);
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the user whose 2FA should be reset (required)'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the reset succeeded'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'id' => $schema->number()->description('Numeric ID of the user'),
'username' => $schema->string()->description('Username of the user'),
];
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Asset;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('restore_asset')]
#[Title('Restore Asset')]
#[Description('Restore a soft-deleted Snipe-IT asset')]
class RestoreAssetTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'required|integer',
]);
$asset = Asset::withTrashed()->find($request->get('id'));
if (! $asset) {
return Response::make(Response::error(trans('mcp.asset_not_found')));
}
if (! $asset->deleted_at) {
return Response::make(Response::error(trans('mcp.asset_not_deleted')));
}
if (! Gate::allows('delete', Asset::class)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$asset->restore();
return Response::make(
Response::text(trans('mcp.asset_restored', ['asset_tag' => $asset->asset_tag]))
)->withStructuredContent([
'success' => true,
'message' => trans('mcp.asset_restored', ['asset_tag' => $asset->asset_tag]),
'id' => $asset->id,
'asset_tag' => $asset->asset_tag,
]);
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the asset to restore (required)'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the restore succeeded'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'id' => $schema->number()->description('Numeric ID of the restored asset'),
'asset_tag' => $schema->string()->description('Asset tag of the restored asset'),
];
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
namespace App\Mcp\Tools;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('restore_user')]
#[Title('Restore User')]
#[Description('Restore a soft-deleted Snipe-IT user')]
class RestoreUserTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
if (! Gate::allows('delete', User::class)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$request->validate([
'id' => 'required|integer',
]);
$user = User::withTrashed()->find($request->get('id'));
if (! $user) {
return Response::make(Response::error(trans('mcp.user_not_found')));
}
if (! $user->deleted_at) {
return Response::make(Response::error(trans('mcp.user_not_deleted')));
}
if (! Gate::allows('delete', User::class)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$user->restore();
return Response::make(
Response::text(trans('mcp.user_restored', ['username' => $user->username]))
)->withStructuredContent([
'success' => true,
'message' => trans('mcp.user_restored', ['username' => $user->username]),
'id' => $user->id,
'username' => $user->username,
]);
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the user to restore (required)'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the restore succeeded'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'id' => $schema->number()->description('Numeric ID of the restored user'),
'username' => $schema->string()->description('Username of the restored user'),
];
}
}
+105
View File
@@ -0,0 +1,105 @@
<?php
namespace App\Mcp\Tools;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Password;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('send_password_reset')]
#[Title('Send Password Reset Email')]
#[Description('Send a password reset link to a Snipe-IT user identified by numeric ID, username, or email address. The user must be active, have an email address, and not be an LDAP-imported account.')]
class SendPasswordResetTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'username' => 'nullable|string|max:191',
'email' => 'nullable|string|max:191',
]);
$user = $this->resolveUser($request);
if (! $user) {
return Response::make(Response::error(trans('mcp.user_not_found')));
}
if (! Gate::allows('view', $user)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
if (! $user->activated) {
return Response::make(Response::error(trans('mcp.password_reset_user_inactive', ['username' => $user->username])));
}
if (empty($user->email)) {
return Response::make(Response::error(trans('mcp.password_reset_no_email', ['username' => $user->username])));
}
if ($user->ldap_import) {
return Response::make(Response::error(trans('mcp.password_reset_ldap_user', ['username' => $user->username])));
}
try {
$result = Password::sendResetLink(['email' => trim($user->email)]);
} catch (\Exception $e) {
return Response::make(Response::error(trans('mcp.password_reset_send_failed', ['error' => $e->getMessage()])));
}
if ($result === Password::RESET_LINK_SENT) {
return Response::make(
Response::text(trans('mcp.password_reset_sent', ['email' => $user->email]))
)->withStructuredContent([
'success' => true,
'message' => trans('mcp.password_reset_sent', ['email' => $user->email]),
'username' => $user->username,
'email' => $user->email,
]);
}
return Response::make(Response::error(trans('mcp.password_reset_send_failed', ['error' => $result])));
}
private function resolveUser(Request $request): ?User
{
if ($request->filled('id')) {
return User::find($request->get('id'));
}
if ($request->filled('username')) {
return User::where('username', $request->get('username'))->first();
}
if ($request->filled('email')) {
return User::where('email', $request->get('email'))->first();
}
return null;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the user'),
'username' => $schema->string()->description('Username of the user'),
'email' => $schema->string()->description('Email address of the user'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the reset email was sent'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'username' => $schema->string()->description('Username of the user'),
'email' => $schema->string()->description('Email address the reset link was sent to'),
];
}
}
+105
View File
@@ -0,0 +1,105 @@
<?php
namespace App\Mcp\Tools;
use App\Models\AssetModel;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('show_asset_model')]
#[Title('Show Asset Model Details')]
#[Description('Look up a single Snipe-IT asset model by numeric ID or name and return its full details')]
class ShowAssetModelTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
]);
$model = $this->resolveModel($request);
if ($model === false) {
return Response::make(Response::error(trans('mcp.id_or_name_required')));
}
if (! $model) {
return Response::make(Response::error(trans('mcp.asset_model_not_found')));
}
if (! Gate::allows('view', $model)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$model->loadCount('assets as assets_count');
return Response::make(
Response::text(trans('mcp.asset_model_found', ['name' => $model->name]))
)->withStructuredContent([
'id' => $model->id,
'name' => $model->name,
'model_number' => $model->model_number,
'category_id' => $model->category_id,
'category' => $model->category?->name,
'manufacturer_id' => $model->manufacturer_id,
'manufacturer' => $model->manufacturer?->name,
'depreciation_id' => $model->depreciation_id,
'depreciation' => $model->depreciation?->name,
'assets_count' => $model->assets_count,
'eol' => $model->eol,
'min_amt' => $model->min_amt,
'notes' => $model->notes,
'created_at' => $model->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $model->updated_at?->format('Y-m-d H:i:s'),
]);
}
private function resolveModel(Request $request): AssetModel|false|null
{
if ($request->filled('id')) {
return AssetModel::with('category', 'manufacturer', 'depreciation')->find($request->get('id'));
}
if ($request->filled('name')) {
return AssetModel::with('category', 'manufacturer', 'depreciation')->where('name', $request->get('name'))->first();
}
return false;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the asset model to look up'),
'name' => $schema->string()->description('Name of the asset model to look up'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric asset model ID'),
'name' => $schema->string()->description('Asset model name'),
'model_number' => $schema->string()->description('Model number'),
'category_id' => $schema->number()->description('Category ID'),
'category' => $schema->string()->description('Category name'),
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
'manufacturer' => $schema->string()->description('Manufacturer name'),
'depreciation_id' => $schema->number()->description('Depreciation schedule ID'),
'depreciation' => $schema->string()->description('Depreciation schedule name'),
'assets_count' => $schema->number()->description('Number of assets using this model'),
'eol' => $schema->number()->description('End of life in months'),
'min_amt' => $schema->number()->description('Minimum quantity alert threshold'),
'notes' => $schema->string()->description('Notes'),
'created_at' => $schema->string()->description('Creation timestamp'),
'updated_at' => $schema->string()->description('Last update timestamp'),
];
}
}
+124
View File
@@ -0,0 +1,124 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Asset;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('show_asset')]
#[Title('Show Asset Details')]
#[Description('Look up a single Snipe-IT asset by asset tag or numeric ID and return its full details')]
class ShowAssetTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'asset_tag' => 'nullable|max:100',
'serial' => 'nullable|string|max:255',
'id' => 'nullable|integer',
]);
$with = ['status', 'model.category', 'model.manufacturer', 'location', 'defaultLoc', 'company', 'supplier', 'adminuser'];
$asset = null;
if ($request->filled('asset_tag')) {
$asset = Asset::where('asset_tag', $request->get('asset_tag'))->with($with)->first();
} elseif ($request->filled('serial')) {
$asset = Asset::where('serial', $request->get('serial'))->with($with)->first();
} elseif ($request->filled('id')) {
$asset = Asset::with($with)->find($request->get('id'));
}
if (! $asset) {
return Response::make(
Response::error(trans('mcp.asset_not_found'))
);
}
if (! Gate::allows('view', $asset)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
return Response::make(
Response::text(trans('mcp.asset_found', ['asset_tag' => $asset->asset_tag]))
)->withStructuredContent($this->formatAsset($asset));
}
private function formatAsset(Asset $asset): array
{
return [
'id' => $asset->id,
'asset_tag' => $asset->asset_tag,
'name' => $asset->name,
'serial' => $asset->serial,
'status' => $asset->status?->name,
'status_type' => $asset->status?->getStatuslabelType(),
'model' => $asset->model?->name,
'model_number' => $asset->model?->model_number,
'category' => $asset->model?->category?->name,
'manufacturer' => $asset->model?->manufacturer?->name,
'company' => $asset->company?->name,
'location' => $asset->location?->name,
'rtd_location' => $asset->defaultLoc?->name,
'supplier' => $asset->supplier?->name,
'assigned_to_id' => $asset->assigned_to,
'assigned_to_type' => $asset->assigned_type ? class_basename($asset->assigned_type) : null,
'notes' => $asset->notes,
'order_number' => $asset->order_number,
'purchase_date' => $asset->purchase_date?->format('Y-m-d'),
'purchase_cost' => $asset->purchase_cost,
'warranty_months' => $asset->warranty_months,
'last_checkout' => $asset->last_checkout,
'last_checkin' => $asset->last_checkin,
'expected_checkin' => $asset->expected_checkin,
'last_audit_date' => $asset->last_audit_date,
'created_at' => $asset->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $asset->updated_at?->format('Y-m-d H:i:s'),
];
}
public function schema(JsonSchema $schema): array
{
return [
'asset_tag' => $schema->string()
->description('The asset tag of the asset to look up'),
'serial' => $schema->string()
->description('The serial number of the asset to look up'),
'id' => $schema->number()
->description('The numeric ID of the asset to look up'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric asset ID'),
'asset_tag' => $schema->string()->description('Asset tag'),
'name' => $schema->string()->description('Asset name'),
'serial' => $schema->string()->description('Serial number'),
'status' => $schema->string()->description('Status label name'),
'status_type' => $schema->string()->description('Status type: deployable, pending, or archived'),
'model' => $schema->string()->description('Asset model name'),
'model_number' => $schema->string()->description('Model number'),
'category' => $schema->string()->description('Category name'),
'manufacturer' => $schema->string()->description('Manufacturer name'),
'company' => $schema->string()->description('Company name'),
'location' => $schema->string()->description('Current location name'),
'rtd_location' => $schema->string()->description('Default return-to-deploy location name'),
'assigned_to_id' => $schema->number()->description('ID of the entity this asset is currently assigned to'),
'assigned_to_type' => $schema->string()->description('Type of entity assigned to: User, Asset, or Location'),
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
'purchase_cost' => $schema->string()->description('Purchase cost'),
'last_checkout' => $schema->string()->description('Date of last checkout'),
'last_checkin' => $schema->string()->description('Date of last checkin'),
];
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Category;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('show_category')]
#[Title('Show Category Details')]
#[Description('Look up a single Snipe-IT category by numeric ID or name and return its full details')]
class ShowCategoryTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
]);
$withCounts = [
'showableAssets as assets_count',
'accessories as accessories_count',
'consumables as consumables_count',
'components as components_count',
'licenses as licenses_count',
];
$category = null;
if ($request->filled('id')) {
$category = Category::withCount($withCounts)->find($request->get('id'));
} elseif ($request->filled('name')) {
$category = Category::withCount($withCounts)->where('name', $request->get('name'))->first();
} else {
return Response::make(Response::error(trans('mcp.id_or_name_required')));
}
if (! $category) {
return Response::make(Response::error(trans('mcp.category_not_found')));
}
if (! Gate::allows('view', $category)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
return Response::make(
Response::text(trans('mcp.category_found', ['name' => $category->name]))
)->withStructuredContent([
'id' => $category->id,
'name' => $category->name,
'category_type' => $category->category_type,
'assets_count' => $category->assets_count,
'accessories_count' => $category->accessories_count,
'consumables_count' => $category->consumables_count,
'components_count' => $category->components_count,
'licenses_count' => $category->licenses_count,
'notes' => $category->notes,
'created_at' => $category->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $category->updated_at?->format('Y-m-d H:i:s'),
]);
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the category to look up'),
'name' => $schema->string()->description('Name of the category to look up'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric category ID'),
'name' => $schema->string()->description('Category name'),
'category_type' => $schema->string()->description('Category type: asset, accessory, consumable, component, or license'),
'assets_count' => $schema->number()->description('Number of assets in this category'),
'accessories_count' => $schema->number()->description('Number of accessories in this category'),
'consumables_count' => $schema->number()->description('Number of consumables in this category'),
'components_count' => $schema->number()->description('Number of components in this category'),
'licenses_count' => $schema->number()->description('Number of licenses in this category'),
'notes' => $schema->string()->description('Notes'),
'created_at' => $schema->string()->description('Creation timestamp'),
'updated_at' => $schema->string()->description('Last update timestamp'),
];
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Company;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('show_company')]
#[Title('Show Company Details')]
#[Description('Look up a single Snipe-IT company by numeric ID or name and return its full details')]
class ShowCompanyTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
]);
$company = null;
if ($request->filled('id')) {
$company = Company::withCount([
'assets as assets_count' => fn ($q) => $q->AssetsForShow(),
])->withCount('users as users_count')->find($request->get('id'));
} elseif ($request->filled('name')) {
$company = Company::withCount([
'assets as assets_count' => fn ($q) => $q->AssetsForShow(),
])->withCount('users as users_count')->where('name', $request->get('name'))->first();
} else {
return Response::make(Response::error(trans('mcp.id_or_name_required')));
}
if (! $company) {
return Response::make(Response::error(trans('mcp.company_not_found')));
}
if (! Gate::allows('view', $company)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
return Response::make(
Response::text(trans('mcp.company_found', ['name' => $company->name]))
)->withStructuredContent([
'id' => $company->id,
'name' => $company->name,
'phone' => $company->phone,
'fax' => $company->fax,
'email' => $company->email,
'assets_count' => $company->assets_count,
'users_count' => $company->users_count,
'notes' => $company->notes,
'created_at' => $company->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $company->updated_at?->format('Y-m-d H:i:s'),
]);
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the company to look up'),
'name' => $schema->string()->description('Name of the company to look up'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric company ID'),
'name' => $schema->string()->description('Company name'),
'phone' => $schema->string()->description('Company phone number'),
'fax' => $schema->string()->description('Company fax number'),
'email' => $schema->string()->description('Company email address'),
'assets_count' => $schema->number()->description('Number of assets belonging to this company'),
'users_count' => $schema->number()->description('Number of users belonging to this company'),
'notes' => $schema->string()->description('Notes'),
'created_at' => $schema->string()->description('Creation timestamp'),
'updated_at' => $schema->string()->description('Last update timestamp'),
];
}
}
+107
View File
@@ -0,0 +1,107 @@
<?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('show_consumable')]
#[Title('Show Consumable Details')]
#[Description('Look up a single Snipe-IT consumable by numeric ID or name and return its full details')]
class ShowConsumableTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
]);
$consumable = null;
if ($request->filled('id')) {
$consumable = Consumable::with('company', 'category', 'manufacturer', 'supplier', 'location')->find($request->get('id'));
} elseif ($request->filled('name')) {
$consumable = Consumable::with('company', 'category', 'manufacturer', 'supplier', 'location')->where('name', $request->get('name'))->first();
} else {
return Response::make(Response::error(trans('mcp.id_or_name_required')));
}
if (! $consumable) {
return Response::make(Response::error(trans('mcp.consumable_not_found')));
}
if (! Gate::allows('view', $consumable)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$usersCount = $consumable->users()->count();
return Response::make(
Response::text(trans('mcp.consumable_found', ['name' => $consumable->name]))
)->withStructuredContent([
'id' => $consumable->id,
'name' => $consumable->name,
'qty' => $consumable->qty,
'users_count' => $usersCount,
'min_amt' => $consumable->min_amt,
'category_id' => $consumable->category_id,
'category' => $consumable->category?->name,
'manufacturer_id' => $consumable->manufacturer_id,
'manufacturer' => $consumable->manufacturer?->name,
'company_id' => $consumable->company_id,
'company' => $consumable->company?->name,
'location_id' => $consumable->location_id,
'location' => $consumable->location?->name,
'purchase_cost' => $consumable->purchase_cost,
'purchase_date' => $consumable->purchase_date?->format('Y-m-d'),
'order_number' => $consumable->order_number,
'model_number' => $consumable->model_number,
'notes' => $consumable->notes,
'created_at' => $consumable->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $consumable->updated_at?->format('Y-m-d H:i:s'),
]);
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the consumable to look up'),
'name' => $schema->string()->description('Name of the consumable to look up'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric consumable ID'),
'name' => $schema->string()->description('Consumable name'),
'qty' => $schema->number()->description('Total quantity in stock'),
'users_count' => $schema->number()->description('Number of units checked out'),
'min_amt' => $schema->number()->description('Minimum quantity alert threshold'),
'category_id' => $schema->number()->description('Category ID'),
'category' => $schema->string()->description('Category name'),
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
'manufacturer' => $schema->string()->description('Manufacturer name'),
'company_id' => $schema->number()->description('Company ID'),
'company' => $schema->string()->description('Company name'),
'location_id' => $schema->number()->description('Location ID'),
'location' => $schema->string()->description('Location name'),
'purchase_cost' => $schema->string()->description('Purchase cost'),
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
'order_number' => $schema->string()->description('Order number'),
'model_number' => $schema->string()->description('Model number'),
'notes' => $schema->string()->description('Notes'),
'created_at' => $schema->string()->description('Creation timestamp'),
'updated_at' => $schema->string()->description('Last updated timestamp'),
];
}
}
+87
View File
@@ -0,0 +1,87 @@
<?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('show_depreciation')]
#[Title('Show Depreciation Details')]
#[Description('Look up a single Snipe-IT depreciation schedule by numeric ID or name and return its full details')]
class ShowDepreciationTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
]);
$depreciation = $this->resolveDepreciation($request);
if ($depreciation === false) {
return Response::make(Response::error(trans('mcp.id_or_name_required')));
}
if (! $depreciation) {
return Response::make(Response::error(trans('mcp.depreciation_not_found')));
}
if (! Gate::allows('view', $depreciation)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$depreciation->loadCount('models as models_count');
return Response::make(
Response::text(trans('mcp.depreciation_found', ['name' => $depreciation->name]))
)->withStructuredContent([
'id' => $depreciation->id,
'name' => $depreciation->name,
'months' => $depreciation->months,
'models_count' => $depreciation->models_count,
'created_at' => $depreciation->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $depreciation->updated_at?->format('Y-m-d H:i:s'),
]);
}
private function resolveDepreciation(Request $request): Depreciation|false|null
{
if ($request->filled('id')) {
return Depreciation::find($request->get('id'));
}
if ($request->filled('name')) {
return Depreciation::where('name', $request->get('name'))->first();
}
return false;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the depreciation to look up'),
'name' => $schema->string()->description('Name of the depreciation to look up'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric depreciation ID'),
'name' => $schema->string()->description('Depreciation name'),
'months' => $schema->number()->description('Depreciation period in months'),
'models_count' => $schema->number()->description('Number of asset models using this depreciation'),
'created_at' => $schema->string()->description('Creation timestamp'),
'updated_at' => $schema->string()->description('Last update timestamp'),
];
}
}
+75
View File
@@ -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('show_group')]
#[Title('Show Group')]
#[Description('Look up a single Snipe-IT permission group by numeric ID or name')]
class ShowGroupTool 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::withCount('users as users_count')->find($request->get('id'));
} elseif ($request->filled('name')) {
$group = Group::withCount('users as users_count')
->where('name', $request->get('name'))
->first();
} else {
return Response::make(Response::error(trans('mcp.id_or_name_required')));
}
if (! $group) {
return Response::make(Response::error(trans('mcp.group_not_found')));
}
return Response::make(
Response::text(trans('mcp.group_found', ['name' => $group->name]))
)->withStructuredContent([
'id' => $group->id,
'name' => $group->name,
'notes' => $group->notes,
'permissions' => $group->decodePermissions(),
'users_count' => $group->users_count,
]);
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric group ID'),
'name' => $schema->string()->description('Group name to look up'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric group ID')->required(),
'name' => $schema->string()->description('Group name')->required(),
'notes' => $schema->string()->description('Notes about the group'),
'permissions' => $schema->object()->description('Decoded permissions array'),
'users_count' => $schema->number()->description('Number of users in this group'),
];
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace App\Mcp\Tools;
use App\Models\License;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('show_license')]
#[Title('Show License Details')]
#[Description('Look up a single Snipe-IT license by numeric ID or name and return its full details including seat counts')]
class ShowLicenseTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
]);
if ($request->filled('id')) {
$license = License::with('company', 'manufacturer', 'supplier', 'category')
->withCount('freeSeats as free_seats_count')
->find($request->get('id'));
} elseif ($request->filled('name')) {
$license = License::with('company', 'manufacturer', 'supplier', 'category')
->withCount('freeSeats as free_seats_count')
->where('name', $request->get('name'))
->first();
} else {
return Response::make(Response::error(trans('mcp.id_or_name_required')));
}
if (! $license) {
return Response::make(Response::error(trans('mcp.license_not_found')));
}
if (! Gate::allows('view', $license)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$assignedCount = $license->assignedCount()->count();
return Response::make(
Response::text(trans('mcp.license_found', ['name' => $license->name]))
)->withStructuredContent([
'id' => $license->id,
'name' => $license->name,
'serial' => $license->serial,
'seats' => $license->seats,
'free_seats' => $license->free_seats_count,
'assigned_seats' => $assignedCount,
'category' => $license->category?->name,
'category_id' => $license->category_id,
'manufacturer' => $license->manufacturer?->name,
'manufacturer_id' => $license->manufacturer_id,
'company' => $license->company?->name,
'company_id' => $license->company_id,
'supplier' => $license->supplier?->name,
'supplier_id' => $license->supplier_id,
'license_name' => $license->license_name,
'license_email' => $license->license_email,
'maintained' => (bool) $license->maintained,
'reassignable' => (bool) $license->reassignable,
'purchase_date' => $license->purchase_date?->format('Y-m-d'),
'purchase_cost' => $license->purchase_cost,
'purchase_order' => $license->purchase_order,
'order_number' => $license->order_number,
'expiration_date' => $license->expiration_date?->format('Y-m-d'),
'termination_date' => $license->termination_date?->format('Y-m-d'),
'notes' => $license->notes,
'created_at' => $license->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $license->updated_at?->format('Y-m-d H:i:s'),
]);
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric license ID'),
'name' => $schema->string()->description('License name to look up'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric license ID')->required(),
'name' => $schema->string()->description('License name')->required(),
'seats' => $schema->number()->description('Total seat count'),
'free_seats' => $schema->number()->description('Number of available (unassigned) seats'),
'assigned_seats' => $schema->number()->description('Number of currently assigned seats'),
];
}
}
+109
View File
@@ -0,0 +1,109 @@
<?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('show_location')]
#[Title('Show Location Details')]
#[Description('Look up a single Snipe-IT location by numeric ID or name and return its full details')]
class ShowLocationTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
]);
$location = $this->resolveLocation($request);
if ($location === false) {
return Response::make(Response::error(trans('mcp.id_or_name_required')));
}
if (! $location) {
return Response::make(Response::error(trans('mcp.location_not_found')));
}
if (! Gate::allows('view', $location)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$location->loadCount('assets as assets_count', 'users as users_count', 'children as children_count');
return Response::make(
Response::text(trans('mcp.location_found', ['name' => $location->name]))
)->withStructuredContent([
'id' => $location->id,
'name' => $location->name,
'address' => $location->address,
'address2' => $location->address2,
'city' => $location->city,
'state' => $location->state,
'country' => $location->country,
'zip' => $location->zip,
'phone' => $location->phone,
'fax' => $location->fax,
'parent_id' => $location->parent_id,
'parent' => $location->parent?->name,
'assets_count' => $location->assets_count,
'users_count' => $location->users_count,
'children_count' => $location->children_count,
'created_at' => $location->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $location->updated_at?->format('Y-m-d H:i:s'),
]);
}
private function resolveLocation(Request $request): Location|false|null
{
if ($request->filled('id')) {
return Location::with('parent')->find($request->get('id'));
}
if ($request->filled('name')) {
return Location::with('parent')->where('name', $request->get('name'))->first();
}
return false;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the location to look up'),
'name' => $schema->string()->description('Name of the location to look up'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric location ID'),
'name' => $schema->string()->description('Location name'),
'address' => $schema->string()->description('Street address'),
'address2' => $schema->string()->description('Address line 2'),
'city' => $schema->string()->description('City'),
'state' => $schema->string()->description('State'),
'country' => $schema->string()->description('Country'),
'zip' => $schema->string()->description('Zip code'),
'phone' => $schema->string()->description('Phone number'),
'fax' => $schema->string()->description('Fax number'),
'parent_id' => $schema->number()->description('Parent location ID'),
'parent' => $schema->string()->description('Parent location name'),
'assets_count' => $schema->number()->description('Number of assets at this location'),
'users_count' => $schema->number()->description('Number of users at this location'),
'children_count' => $schema->number()->description('Number of child locations'),
'created_at' => $schema->string()->description('Creation timestamp'),
'updated_at' => $schema->string()->description('Last update timestamp'),
];
}
}
+97
View File
@@ -0,0 +1,97 @@
<?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('show_manufacturer')]
#[Title('Show Manufacturer')]
#[Description('Show details of a Snipe-IT manufacturer identified by numeric ID or name')]
class ShowManufacturerTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
]);
$manufacturer = $this->resolveManufacturer($request);
if (! $manufacturer) {
if (! $request->filled('id') && ! $request->filled('name')) {
return Response::make(Response::error(trans('mcp.id_or_name_required')));
}
return Response::make(Response::error(trans('mcp.manufacturer_not_found')));
}
if (! Gate::allows('view', $manufacturer)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$manufacturer->loadCount('assets as assets_count');
return Response::make(
Response::text(trans('mcp.manufacturer_found', ['name' => $manufacturer->name]))
)->withStructuredContent([
'id' => $manufacturer->id,
'name' => $manufacturer->name,
'url' => $manufacturer->url,
'support_url' => $manufacturer->support_url,
'support_email' => $manufacturer->support_email,
'support_phone' => $manufacturer->support_phone,
'warranty_lookup_url' => $manufacturer->warranty_lookup_url,
'assets_count' => $manufacturer->assets_count,
'notes' => $manufacturer->notes,
'created_at' => $manufacturer->created_at?->toISOString(),
'updated_at' => $manufacturer->updated_at?->toISOString(),
]);
}
private function resolveManufacturer(Request $request): ?Manufacturer
{
if ($request->filled('id')) {
return Manufacturer::find($request->get('id'));
}
if ($request->filled('name')) {
return Manufacturer::where('name', $request->get('name'))->first();
}
return null;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the manufacturer to show'),
'name' => $schema->string()->description('Name of the manufacturer to show'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the manufacturer'),
'name' => $schema->string()->description('Manufacturer name')->required(),
'url' => $schema->string()->description('Manufacturer website URL'),
'support_url' => $schema->string()->description('Support website URL'),
'support_email' => $schema->string()->description('Support email address'),
'support_phone' => $schema->string()->description('Support phone number'),
'warranty_lookup_url' => $schema->string()->description('Warranty lookup URL'),
'assets_count' => $schema->number()->description('Number of assets from this manufacturer'),
'notes' => $schema->string()->description('Notes'),
'created_at' => $schema->string()->description('Creation timestamp'),
'updated_at' => $schema->string()->description('Last update timestamp'),
];
}
}
+101
View File
@@ -0,0 +1,101 @@
<?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('show_status_label')]
#[Title('Show Status Label')]
#[Description('Show details of a Snipe-IT status label identified by numeric ID or name')]
class ShowStatusLabelTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
]);
$label = $this->resolveStatusLabel($request);
if (! $label) {
if (! $request->filled('id') && ! $request->filled('name')) {
return Response::make(Response::error(trans('mcp.id_or_name_required')));
}
return Response::make(Response::error(trans('mcp.status_label_not_found')));
}
if (! Gate::allows('view', $label)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$label->loadCount('assets as assets_count');
return Response::make(
Response::text(trans('mcp.status_label_found', ['name' => $label->name]))
)->withStructuredContent([
'id' => $label->id,
'name' => $label->name,
'type' => $label->getStatuslabelType(),
'color' => $label->color,
'deployable' => $label->deployable,
'pending' => $label->pending,
'archived' => $label->archived,
'assets_count' => $label->assets_count,
'default_label' => $label->default_label,
'show_in_nav' => $label->show_in_nav,
'notes' => $label->notes,
'created_at' => $label->created_at?->toISOString(),
'updated_at' => $label->updated_at?->toISOString(),
]);
}
private function resolveStatusLabel(Request $request): ?Statuslabel
{
if ($request->filled('id')) {
return Statuslabel::find($request->get('id'));
}
if ($request->filled('name')) {
return Statuslabel::where('name', $request->get('name'))->first();
}
return null;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the status label to show'),
'name' => $schema->string()->description('Name of the status label to show'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the status label'),
'name' => $schema->string()->description('Status label name')->required(),
'type' => $schema->string()->description('Status label type (deployable, pending, archived, undeployable)'),
'color' => $schema->string()->description('Display color'),
'deployable' => $schema->boolean()->description('Whether status is deployable'),
'pending' => $schema->boolean()->description('Whether status is pending'),
'archived' => $schema->boolean()->description('Whether status is archived'),
'assets_count' => $schema->number()->description('Number of assets with this status'),
'default_label' => $schema->boolean()->description('Whether this is the default label'),
'show_in_nav' => $schema->boolean()->description('Whether to show in navigation'),
'notes' => $schema->string()->description('Notes'),
'created_at' => $schema->string()->description('Creation timestamp'),
'updated_at' => $schema->string()->description('Last update timestamp'),
];
}
}
+111
View File
@@ -0,0 +1,111 @@
<?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('show_supplier')]
#[Title('Show Supplier')]
#[Description('Show details of a Snipe-IT supplier identified by numeric ID or name')]
class ShowSupplierTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
]);
$supplier = $this->resolveSupplier($request);
if (! $supplier) {
if (! $request->filled('id') && ! $request->filled('name')) {
return Response::make(Response::error(trans('mcp.id_or_name_required')));
}
return Response::make(Response::error(trans('mcp.supplier_not_found')));
}
if (! Gate::allows('view', $supplier)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$supplier->loadCount('assets as assets_count', 'licenses as licenses_count');
return Response::make(
Response::text(trans('mcp.supplier_found', ['name' => $supplier->name]))
)->withStructuredContent([
'id' => $supplier->id,
'name' => $supplier->name,
'address' => $supplier->address,
'address2' => $supplier->address2,
'city' => $supplier->city,
'state' => $supplier->state,
'country' => $supplier->country,
'zip' => $supplier->zip,
'phone' => $supplier->phone,
'fax' => $supplier->fax,
'email' => $supplier->email,
'url' => $supplier->url,
'contact' => $supplier->contact,
'notes' => $supplier->notes,
'assets_count' => $supplier->assets_count,
'licenses_count' => $supplier->licenses_count,
'created_at' => $supplier->created_at?->toISOString(),
'updated_at' => $supplier->updated_at?->toISOString(),
]);
}
private function resolveSupplier(Request $request): ?Supplier
{
if ($request->filled('id')) {
return Supplier::find($request->get('id'));
}
if ($request->filled('name')) {
return Supplier::where('name', $request->get('name'))->first();
}
return null;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the supplier to show'),
'name' => $schema->string()->description('Name of the supplier to show'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the supplier'),
'name' => $schema->string()->description('Supplier name')->required(),
'address' => $schema->string()->description('Address line 1'),
'address2' => $schema->string()->description('Address line 2'),
'city' => $schema->string()->description('City'),
'state' => $schema->string()->description('State'),
'country' => $schema->string()->description('Country'),
'zip' => $schema->string()->description('Postal code'),
'phone' => $schema->string()->description('Phone number'),
'fax' => $schema->string()->description('Fax number'),
'email' => $schema->string()->description('Email address'),
'url' => $schema->string()->description('Website URL'),
'contact' => $schema->string()->description('Contact name'),
'notes' => $schema->string()->description('Notes'),
'assets_count' => $schema->number()->description('Number of assets from this supplier'),
'licenses_count' => $schema->number()->description('Number of licenses from this supplier'),
'created_at' => $schema->string()->description('Creation timestamp'),
'updated_at' => $schema->string()->description('Last update timestamp'),
];
}
}
+116
View File
@@ -0,0 +1,116 @@
<?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('show_user')]
#[Title('Show User')]
#[Description('Look up a single Snipe-IT user by numeric ID, username, or email address')]
class ShowUserTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'username' => 'nullable|string|max:191',
'email' => 'nullable|string|max:191',
]);
$with = ['company', 'department', 'userloc', 'manager', 'groups'];
if ($request->filled('id')) {
$user = User::with($with)
->withCount(['assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count'])
->find($request->get('id'));
} elseif ($request->filled('username')) {
$user = User::where('username', $request->get('username'))->with($with)
->withCount(['assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count'])
->first();
} elseif ($request->filled('email')) {
$user = User::where('email', $request->get('email'))->with($with)
->withCount(['assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count'])
->first();
} else {
return Response::make(Response::error(trans('mcp.id_username_or_email_required')));
}
if (! $user) {
return Response::make(Response::error(trans('mcp.user_not_found')));
}
if (! Gate::allows('view', $user)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
return Response::make(
Response::text(trans('mcp.user_found', ['username' => $user->username]))
)->withStructuredContent([
'id' => $user->id,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'username' => $user->username,
'email' => $user->email,
'employee_num' => $user->employee_num,
'jobtitle' => $user->jobtitle,
'phone' => $user->phone,
'mobile' => $user->mobile,
'company' => $user->company?->name,
'company_id' => $user->company_id,
'department' => $user->department?->name,
'department_id' => $user->department_id,
'location' => $user->userloc?->name,
'location_id' => $user->location_id,
'manager' => $user->manager ? trim($user->manager->first_name.' '.$user->manager->last_name) : null,
'manager_id' => $user->manager_id,
'activated' => (bool) $user->activated,
'notes' => $user->notes,
'start_date' => $user->start_date?->toDateString(),
'end_date' => $user->end_date?->toDateString(),
'vip' => (bool) $user->vip,
'remote' => (bool) $user->remote,
'website' => $user->website,
'address' => $user->address,
'city' => $user->city,
'state' => $user->state,
'country' => $user->country,
'zip' => $user->zip,
'assets_count' => $user->assets_count,
'licenses_count' => $user->licenses_count,
'accessories_count' => $user->accessories_count,
'consumables_count' => $user->consumables_count,
'last_login' => $user->last_login?->toDateTimeString(),
'created_at' => $user->created_at?->toDateTimeString(),
'updated_at' => $user->updated_at?->toDateTimeString(),
]);
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric user ID'),
'username' => $schema->string()->description('Username to look up'),
'email' => $schema->string()->description('Email address to look up'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric user ID')->required(),
'username' => $schema->string()->description('Username')->required(),
'email' => $schema->string()->description('Email address'),
'first_name' => $schema->string()->description('First name'),
'last_name' => $schema->string()->description('Last name'),
];
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Accessory;
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('update_accessory')]
#[Title('Update Accessory')]
#[Description('Update fields on a Snipe-IT accessory identified by numeric ID or name')]
class UpdateAccessoryTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
'new_name' => 'nullable|string|max:255',
'category_id' => 'nullable|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',
]);
$accessory = $this->resolveAccessory($request);
if (! $accessory) {
return Response::make(Response::error(trans('mcp.accessory_not_found')));
}
if (! Gate::allows('update', $accessory)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$updatable = [
'category_id', 'qty', 'model_number', 'manufacturer_id',
'supplier_id', 'location_id', 'order_number', 'purchase_cost',
'purchase_date', 'min_amt', 'requestable', 'notes',
];
foreach ($updatable as $field) {
if ($request->filled($field)) {
$accessory->{$field} = $request->get($field);
}
}
if ($request->filled('new_name')) {
$accessory->name = $request->get('new_name');
}
if ($request->filled('company_id')) {
$accessory->company_id = Company::getIdForCurrentUser($request->get('company_id'));
}
if ($accessory->save()) {
return Response::make(
Response::text(trans('mcp.accessory_updated', ['name' => $accessory->name]))
)->withStructuredContent([
'success' => true,
'message' => trans('mcp.accessory_updated', ['name' => $accessory->name]),
'id' => $accessory->id,
'name' => $accessory->name,
]);
}
return Response::make(Response::error(trans('mcp.update_failed', ['error' => $accessory->getErrors()->first()])));
}
private function resolveAccessory(Request $request): ?Accessory
{
if ($request->filled('id')) {
return Accessory::find($request->get('id'));
}
if ($request->filled('name')) {
return Accessory::where('name', $request->get('name'))->first();
}
return null;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID to identify the accessory'),
'name' => $schema->string()->description('Name to identify the accessory'),
'new_name' => $schema->string()->description('New name (renames the accessory)'),
'category_id' => $schema->number()->description('Category ID'),
'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'),
'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 alert threshold'),
'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 update succeeded'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'id' => $schema->number()->description('Numeric ID of the accessory'),
'name' => $schema->string()->description('Name of the accessory'),
];
}
}
+111
View File
@@ -0,0 +1,111 @@
<?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('update_asset_model')]
#[Title('Update Asset Model')]
#[Description('Update fields on a Snipe-IT asset model identified by numeric ID or name')]
class UpdateAssetModelTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
'new_name' => 'nullable|string|max:255',
'category_id' => 'nullable|integer|exists:categories,id',
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
'depreciation_id' => 'nullable|integer|exists:depreciations,id',
'model_number' => 'nullable|string|max:255',
'eol' => 'nullable|integer|min:0|max:240',
'min_amt' => 'nullable|integer|min:0',
'notes' => 'nullable|string',
'requestable' => 'nullable|boolean',
'require_serial' => 'nullable|boolean',
]);
$model = $this->resolveModel($request);
if (! $model) {
return Response::make(Response::error(trans('mcp.asset_model_not_found')));
}
if (! Gate::allows('update', $model)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
if ($request->filled('new_name')) {
$model->name = $request->get('new_name');
}
foreach (['category_id', 'manufacturer_id', 'depreciation_id', 'model_number', 'eol', 'min_amt', 'notes', 'requestable', 'require_serial'] as $field) {
if ($request->filled($field)) {
$model->{$field} = $request->get($field);
}
}
if ($model->save()) {
return Response::make(
Response::text(trans('mcp.asset_model_updated', ['name' => $model->name]))
)->withStructuredContent([
'success' => true,
'message' => trans('mcp.asset_model_updated', ['name' => $model->name]),
'id' => $model->id,
'name' => $model->name,
]);
}
return Response::make(Response::error(trans('mcp.update_failed', ['error' => $model->getErrors()->first()])));
}
private function resolveModel(Request $request): ?AssetModel
{
if ($request->filled('id')) {
return AssetModel::find($request->get('id'));
}
if ($request->filled('name')) {
return AssetModel::where('name', $request->get('name'))->first();
}
return null;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID to identify the asset model'),
'name' => $schema->string()->description('Name to identify the asset model'),
'new_name' => $schema->string()->description('New name (renames the asset model)'),
'category_id' => $schema->number()->description('Category ID'),
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
'depreciation_id' => $schema->number()->description('Depreciation schedule ID'),
'model_number' => $schema->string()->description('Model number'),
'eol' => $schema->number()->description('End of life in months (0-240)'),
'min_amt' => $schema->number()->description('Minimum quantity alert threshold'),
'notes' => $schema->string()->description('Notes'),
'requestable' => $schema->boolean()->description('Whether the model can be requested'),
'require_serial' => $schema->boolean()->description('Whether serial numbers are required'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the update succeeded'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'id' => $schema->number()->description('Numeric ID of the asset model'),
'name' => $schema->string()->description('Name of the asset model'),
];
}
}
+145
View File
@@ -0,0 +1,145 @@
<?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('update_asset')]
#[Title('Update Asset')]
#[Description('Update fields on a Snipe-IT asset identified by asset tag, serial, or numeric ID')]
class UpdateAssetTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'asset_tag' => 'nullable|max:100',
'serial' => 'nullable|string|max:255',
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
'new_asset_tag' => 'nullable|string|max:255',
'new_serial' => 'nullable|string|max:255',
'status_id' => 'nullable|integer|exists:status_labels,id',
'model_id' => 'nullable|integer|exists:models,id',
'notes' => 'nullable|string|max:65535',
'order_number' => 'nullable|string|max:255',
'purchase_date' => 'nullable|date',
'purchase_cost' => 'nullable|numeric',
'warranty_months' => 'nullable|integer',
'location_id' => 'nullable|integer|exists:locations,id',
'rtd_location_id' => 'nullable|integer|exists:locations,id',
'supplier_id' => 'nullable|integer|exists:suppliers,id',
'requestable' => 'nullable|boolean',
'byod' => 'nullable|boolean',
'asset_eol_date' => 'nullable|date',
'expected_checkin' => 'nullable|date',
'next_audit_date' => 'nullable|date',
]);
$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')));
}
$updatable = [
'name', 'status_id', 'model_id', 'notes', 'order_number',
'purchase_date', 'purchase_cost', 'warranty_months',
'location_id', 'rtd_location_id', 'supplier_id',
'requestable', 'byod', 'asset_eol_date', 'expected_checkin', 'next_audit_date',
];
foreach ($updatable as $field) {
if ($request->filled($field)) {
$asset->{$field} = $request->get($field);
}
}
// new_asset_tag / new_serial let callers change the identifiers without
// conflicting with the asset_tag/serial used to look up the asset
if ($request->filled('new_asset_tag')) {
$asset->asset_tag = $request->get('new_asset_tag');
}
if ($request->filled('new_serial')) {
$asset->serial = $request->get('new_serial');
}
if ($asset->save()) {
return Response::make(
Response::text(trans('mcp.asset_updated', ['asset_tag' => $asset->asset_tag]))
)->withStructuredContent([
'success' => true,
'message' => trans('mcp.asset_updated', ['asset_tag' => $asset->asset_tag]),
'asset_tag' => $asset->asset_tag,
'id' => $asset->id,
]);
}
return Response::make(Response::error(trans('mcp.update_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'))->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 to identify the asset'),
'serial' => $schema->string()->description('Serial number to identify the asset'),
'id' => $schema->number()->description('Numeric ID to identify the asset'),
'name' => $schema->string()->description('New display name'),
'new_asset_tag' => $schema->string()->description('New asset tag (renames the asset tag itself)'),
'new_serial' => $schema->string()->description('New serial number'),
'status_id' => $schema->number()->description('Status label ID'),
'model_id' => $schema->number()->description('Model ID'),
'notes' => $schema->string()->description('Notes'),
'order_number' => $schema->string()->description('Order number'),
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
'purchase_cost' => $schema->number()->description('Purchase cost'),
'warranty_months' => $schema->number()->description('Warranty length in months'),
'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'),
'requestable' => $schema->boolean()->description('Whether the asset is user-requestable'),
'byod' => $schema->boolean()->description('Bring-your-own-device flag'),
'asset_eol_date' => $schema->string()->description('Asset end-of-life date (YYYY-MM-DD)'),
'expected_checkin' => $schema->string()->description('Expected check-in date (YYYY-MM-DD)'),
'next_audit_date' => $schema->string()->description('Next scheduled audit date (YYYY-MM-DD)'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the update succeeded'),
'error' => $schema->boolean()->description('True if the update failed'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'asset_tag' => $schema->string()->description('Asset tag of the updated asset'),
'id' => $schema->number()->description('Numeric ID of the updated asset'),
];
}
}
+105
View File
@@ -0,0 +1,105 @@
<?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('update_category')]
#[Title('Update Category')]
#[Description('Update fields on a Snipe-IT category identified by numeric ID or name')]
class UpdateCategoryTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
'new_name' => 'nullable|string|max:255',
'category_type' => 'nullable|string|in:asset,accessory,consumable,component,license',
'notes' => 'nullable|string',
'checkin_email' => 'nullable|boolean',
'require_acceptance' => 'nullable|boolean',
'use_default_eula' => 'nullable|boolean',
]);
$category = $this->resolveCategory($request);
if (! $category) {
return Response::make(Response::error(trans('mcp.category_not_found')));
}
if (! Gate::allows('update', $category)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
if ($request->filled('new_name')) {
$category->name = $request->get('new_name');
}
foreach (['category_type', 'notes', 'checkin_email', 'require_acceptance', 'use_default_eula'] as $field) {
if ($request->filled($field)) {
$category->{$field} = $request->get($field);
}
}
if ($category->save()) {
return Response::make(
Response::text(trans('mcp.category_updated', ['name' => $category->name]))
)->withStructuredContent([
'success' => true,
'message' => trans('mcp.category_updated', ['name' => $category->name]),
'id' => $category->id,
'name' => $category->name,
'category_type' => $category->category_type,
]);
}
return Response::make(Response::error(trans('mcp.update_failed', ['error' => $category->getErrors()->first()])));
}
private function resolveCategory(Request $request): ?Category
{
if ($request->filled('id')) {
return Category::find($request->get('id'));
}
if ($request->filled('name')) {
return Category::where('name', $request->get('name'))->first();
}
return null;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID to identify the category'),
'name' => $schema->string()->description('Name to identify the category'),
'new_name' => $schema->string()->description('New name (renames the category)'),
'category_type' => $schema->string()->description('Category type: asset, accessory, consumable, component, or license'),
'checkin_email' => $schema->boolean()->description('Send checkin email when items are checked in'),
'require_acceptance' => $schema->boolean()->description('Require user acceptance when checking out'),
'use_default_eula' => $schema->boolean()->description('Use the default EULA'),
'notes' => $schema->string()->description('Notes'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the update succeeded'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'id' => $schema->number()->description('Numeric ID of the category'),
'name' => $schema->string()->description('Name of the category'),
'category_type' => $schema->string()->description('Type of the category'),
];
}
}
+101
View File
@@ -0,0 +1,101 @@
<?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('update_company')]
#[Title('Update Company')]
#[Description('Update fields on a Snipe-IT company identified by numeric ID or name')]
class UpdateCompanyTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
'new_name' => 'nullable|string|max:255',
'phone' => 'nullable|string',
'fax' => 'nullable|string',
'email' => 'nullable|string',
'notes' => 'nullable|string',
]);
$company = $this->resolveCompany($request);
if (! $company) {
return Response::make(Response::error(trans('mcp.company_not_found')));
}
if (! Gate::allows('update', $company)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
if ($request->filled('new_name')) {
$company->name = $request->get('new_name');
}
foreach (['phone', 'fax', 'email', 'notes'] as $field) {
if ($request->filled($field)) {
$company->{$field} = $request->get($field);
}
}
if ($company->save()) {
return Response::make(
Response::text(trans('mcp.company_updated', ['name' => $company->name]))
)->withStructuredContent([
'success' => true,
'message' => trans('mcp.company_updated', ['name' => $company->name]),
'id' => $company->id,
'name' => $company->name,
]);
}
return Response::make(Response::error(trans('mcp.update_failed', ['error' => $company->getErrors()->first()])));
}
private function resolveCompany(Request $request): ?Company
{
if ($request->filled('id')) {
return Company::find($request->get('id'));
}
if ($request->filled('name')) {
return Company::where('name', $request->get('name'))->first();
}
return null;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID to identify the company'),
'name' => $schema->string()->description('Name to identify the company'),
'new_name' => $schema->string()->description('New name (renames the company)'),
'phone' => $schema->string()->description('Company phone number'),
'fax' => $schema->string()->description('Company fax number'),
'email' => $schema->string()->description('Company email address'),
'notes' => $schema->string()->description('Notes'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the update succeeded'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'id' => $schema->number()->description('Numeric ID of the company'),
'name' => $schema->string()->description('Name of the company'),
];
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Company;
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('update_component')]
#[Title('Update Component')]
#[Description('Update fields on a Snipe-IT component identified by numeric ID or name')]
class UpdateComponentTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:191',
'new_name' => 'nullable|string|max:191',
'category_id' => 'nullable|integer|exists:categories,id',
'qty' => 'nullable|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',
]);
$component = $this->resolveComponent($request);
if (! $component) {
return Response::make(Response::error(trans('mcp.component_not_found')));
}
if (! Gate::allows('update', $component)) {
return Response::make(Response::error(trans('mcp.unauthorized')));
}
$updatable = [
'category_id', 'qty', 'serial', 'model_number', 'manufacturer_id',
'supplier_id', 'location_id', 'order_number',
'purchase_cost', 'purchase_date', 'min_amt', 'notes',
];
foreach ($updatable as $field) {
if ($request->filled($field)) {
$component->{$field} = $request->get($field);
}
}
if ($request->filled('new_name')) {
$component->name = $request->get('new_name');
}
if ($request->filled('company_id')) {
$component->company_id = Company::getIdForCurrentUser($request->get('company_id'));
}
if ($component->save()) {
return Response::make(
Response::text(trans('mcp.component_updated', ['name' => $component->name]))
)->withStructuredContent([
'success' => true,
'message' => trans('mcp.component_updated', ['name' => $component->name]),
'id' => $component->id,
'name' => $component->name,
]);
}
return Response::make(Response::error(trans('mcp.update_failed', ['error' => $component->getErrors()->first()])));
}
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 to identify the component'),
'name' => $schema->string()->description('Name to identify the component'),
'new_name' => $schema->string()->description('New name (renames the component)'),
'category_id' => $schema->number()->description('Category ID'),
'qty' => $schema->number()->description('Total quantity in stock'),
'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'),
'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 alert threshold'),
'notes' => $schema->string()->description('Notes'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the update succeeded'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'id' => $schema->number()->description('Numeric ID of the component'),
'name' => $schema->string()->description('Name of the component'),
];
}
}

Some files were not shown because too many files have changed in this diff Show More