Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f697ef1d03 | |||
| 256003b675 | |||
| 464db7f473 | |||
| a56426e6f4 | |||
| 19e58a8640 | |||
| d83b64ff32 | |||
| e839d989ec | |||
| b8d2be6c3a | |||
| b264e07327 | |||
| 25a08faa6d | |||
| 926afa6c28 | |||
| e3a042f334 | |||
| 082ebeb27f | |||
| aed11dfce7 | |||
| 4090e05536 | |||
| 49818175cd | |||
| ef4b2349eb | |||
| 926f7dd5f7 | |||
| 8ccc705473 | |||
| c75d0effe2 | |||
| 96a3a11f00 | |||
| 9c97a06c7e | |||
| 2542221fc9 | |||
| 664a1906c1 | |||
| 08b2d0c85d | |||
| dc9f0104f6 | |||
| 6b2f2d68b7 | |||
| 9aa5ba5cd0 | |||
| b74e79b814 | |||
| 7636c2436c | |||
| 0eec6e3688 | |||
| d961714358 | |||
| 51bdc3b020 | |||
| 6a47b4e6a7 | |||
| 656dae04a7 | |||
| 2f3df9a085 | |||
| 0514901cbc | |||
| cc0169d2f7 | |||
| 490ce6fa5d | |||
| b731ec6dd6 | |||
| 91bd2064fd | |||
| deb56f250f | |||
| 7d57ce4679 | |||
| 84fea96949 | |||
| eada5f503c |
@@ -1,6 +1,7 @@
|
||||
# GitHub Copilot Custom Instructions for Snipe-IT
|
||||
|
||||
These instructions guide Copilot to generate code that aligns with modern Laravel 11 standards, PHP 8.2/8.4 features, software engineering principles, and industry best practices to improve software quality, maintainability, and security.
|
||||
These instructions guide Copilot to generate code that aligns with modern Laravel 12 standards, PHP 8.2/8.4 features,
|
||||
software engineering principles, and industry best practices to improve software quality, maintainability, and security.
|
||||
|
||||
## ✅ General Coding Standards
|
||||
|
||||
@@ -22,7 +23,7 @@ These instructions guide Copilot to generate code that aligns with modern Larave
|
||||
- Adopt **final classes** where extension is not intended.
|
||||
- Use **Named Arguments** for improved clarity when calling functions with multiple parameters.
|
||||
|
||||
## ✅ Laravel 11 Project Structure & Conventions
|
||||
## ✅ Laravel 12 Project Structure & Conventions
|
||||
|
||||
- Follow the official Laravel project structure:
|
||||
- `app/Http/Controllers` - Controllers
|
||||
@@ -32,6 +33,7 @@ These instructions guide Copilot to generate code that aligns with modern Larave
|
||||
- `app/Enums` - Enums
|
||||
- `app/Actions` - Single-responsibility action classes
|
||||
- `app/Policies` - Authorization logic
|
||||
- `app/Models/Builders` - Query scoping logic
|
||||
|
||||
- Controllers must:
|
||||
- Use dependency injection.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('audit_location')]
|
||||
#[Title('Audit Location')]
|
||||
#[Description('Review all assets at a location, flag overdue audits and status anomalies')]
|
||||
class AuditLocationPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$location = $request->get('location');
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are conducting an asset audit for location: {$location}.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. Find the location record for "{$location}" (search by name if needed).
|
||||
2. List all assets currently assigned to or located at that location.
|
||||
3. Identify any assets with overdue audit dates (next_audit_date is in the past).
|
||||
4. Flag any assets with unexpected status labels (e.g. archived, pending, or out-for-repair assets that appear to still be at this location).
|
||||
5. Note any assets that have been at this location longer than expected without a check-in or audit event.
|
||||
6. Produce a summary report with: total asset count, assets requiring audit, assets with status anomalies, and any recommended actions.
|
||||
|
||||
Present the findings clearly so they can be acted on or exported.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('location', 'Name or ID of the location to audit', required: true),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('end_of_life_review')]
|
||||
#[Title('End of Life Review')]
|
||||
#[Description('Identify assets that have passed their EOL date or are fully depreciated, and recommend disposition actions')]
|
||||
class EndOfLifeReviewPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$department = $request->get('department');
|
||||
$category = $request->get('category');
|
||||
|
||||
$scope = collect([
|
||||
$department ? "department: {$department}" : null,
|
||||
$category ? "category: {$category}" : null,
|
||||
])->filter()->implode(' and ');
|
||||
|
||||
$scopeLine = $scope
|
||||
? "Limit the review to assets in {$scope}."
|
||||
: 'Review assets across the entire organisation.';
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are conducting an end-of-life and depreciation review. {$scopeLine}
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. List assets that have passed their asset_eol_date (end-of-life date is in the past).
|
||||
2. List assets that are fully depreciated based on their depreciation schedule and purchase date.
|
||||
3. For each identified asset, show: asset tag, name, model, assigned user or location, EOL date, purchase date, and current status.
|
||||
4. Group findings by category for easier review.
|
||||
5. Recommend disposition for each group: retire and replace, redeploy to a lower-demand role, send for repair, or archive.
|
||||
6. Provide a cost summary if purchase cost data is available — total value of end-of-life assets.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('department', 'Limit review to a specific department', required: false),
|
||||
new Argument('category', 'Limit review to a specific asset category', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('expiring_licenses')]
|
||||
#[Title('Expiring Licenses')]
|
||||
#[Description('Review license seat usage and flag licenses expiring within a given number of days')]
|
||||
class ExpiringLicensesPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$days = (int) ($request->get('days', 30));
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are reviewing software license health across the organisation. Focus on licenses expiring within {$days} days.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. List all licenses in the system.
|
||||
2. Identify licenses whose expiration date falls within the next {$days} days.
|
||||
3. For each expiring license, show: license name, total seats, seats in use, seats free, and the expiration date.
|
||||
4. Flag any licenses that are over-deployed (more seats checked out than purchased).
|
||||
5. Flag any licenses that are under-used (many free seats that may indicate unused subscriptions worth cancelling).
|
||||
6. Produce a prioritised action list: renewals needed urgently, over-deployments to resolve, and possible cancellations.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('days', 'Number of days ahead to check for expiring licenses (default: 30)', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('find_available_asset')]
|
||||
#[Title('Find Available Asset')]
|
||||
#[Description('Find an undeployed asset by category or model and optionally check it out to a user')]
|
||||
class FindAvailableAssetPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$category = $request->get('category');
|
||||
$model = $request->get('model');
|
||||
$assignTo = $request->get('assign_to');
|
||||
|
||||
$assetDescription = collect([
|
||||
$category ? "category: {$category}" : null,
|
||||
$model ? "model: {$model}" : null,
|
||||
])->filter()->implode(' / ');
|
||||
|
||||
$assignLine = $assignTo
|
||||
? "If a suitable asset is found, check it out to the user: {$assignTo}."
|
||||
: 'Ask whether the found asset should be checked out to a specific user before proceeding.';
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You need to find an available (undeployed) asset matching {$assetDescription}.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. Search for assets with a Ready-to-Deploy status that match the requested {$assetDescription}.
|
||||
2. If multiple options are available, list them with their asset tags, serial numbers, and any relevant details so the best one can be selected.
|
||||
3. {$assignLine}
|
||||
4. Confirm the final asset tag, serial number, and checkout status once complete.
|
||||
|
||||
If no available assets match, report what was found and suggest alternatives (different models in the same category, or assets currently out for repair that may return soon).
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('category', 'Asset category to search (e.g. Laptop, Monitor)', required: false),
|
||||
new Argument('model', 'Specific model name to search for', required: false),
|
||||
new Argument('assign_to', 'Username to check the asset out to once found', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('inventory_summary')]
|
||||
#[Title('Inventory Summary')]
|
||||
#[Description('Produce a high-level inventory count by category, broken down by deployment status')]
|
||||
class InventorySummaryPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$location = $request->get('location');
|
||||
$department = $request->get('department');
|
||||
|
||||
$scope = collect([
|
||||
$location ? "location: {$location}" : null,
|
||||
$department ? "department: {$department}" : null,
|
||||
])->filter()->implode(' and ');
|
||||
|
||||
$scopeLine = $scope
|
||||
? "Scope the report to {$scope}."
|
||||
: 'Report across the entire organisation.';
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are generating an inventory summary report. {$scopeLine}
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. List assets (filtered by the scope above if provided) and tally counts by status: Deployed, Ready to Deploy, Archived, Pending, Out for Repair.
|
||||
2. Break the deployed count down by asset category (laptops, monitors, phones, etc.).
|
||||
3. List the top 5 models by total quantity.
|
||||
4. Show total purchase value of the inventory if cost data is available.
|
||||
5. Highlight any categories with zero available (Ready to Deploy) assets — potential stock-out risk.
|
||||
6. Present the results as a concise executive summary with a supporting breakdown table.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('location', 'Limit report to a specific location', required: false),
|
||||
new Argument('department', 'Limit report to a specific department', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('offboard_employee')]
|
||||
#[Title('Offboard Employee')]
|
||||
#[Description('Guide through checking in all equipment and licenses from a departing employee and deactivating their account')]
|
||||
class OffboardEmployeePrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$username = $request->get('username');
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are helping offboard a departing employee with username: {$username}.
|
||||
|
||||
Please complete the following offboarding steps using the available tools:
|
||||
|
||||
1. Look up the user account for {$username} and display a summary of everything currently assigned to them (assets, licenses, accessories, consumables).
|
||||
2. Check in all assigned assets from this user.
|
||||
3. Check in all assigned accessories from this user.
|
||||
4. Revoke or check in any license seats assigned to this user.
|
||||
5. Deactivate the user account.
|
||||
6. Provide a final summary of all items that were checked in and confirm the account has been deactivated.
|
||||
|
||||
If any items cannot be checked in automatically, flag them for manual follow-up.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('username', 'Username of the departing employee', required: true),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('onboard_employee')]
|
||||
#[Title('Onboard Employee')]
|
||||
#[Description('Guide through creating a new employee account and assigning appropriate equipment and licenses')]
|
||||
class OnboardEmployeePrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$firstName = $request->get('first_name');
|
||||
$lastName = $request->get('last_name');
|
||||
$department = $request->get('department');
|
||||
$location = $request->get('location');
|
||||
$title = $request->get('title');
|
||||
|
||||
$fullName = trim("{$firstName} {$lastName}");
|
||||
|
||||
$context = collect([
|
||||
$department ? "Department: {$department}" : null,
|
||||
$location ? "Location: {$location}" : null,
|
||||
$title ? "Job title: {$title}" : null,
|
||||
])->filter()->implode("\n");
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are helping onboard a new employee.
|
||||
|
||||
Employee details:
|
||||
- First name: {$firstName}
|
||||
- Last name: {$lastName}
|
||||
{$context}
|
||||
|
||||
Please complete the following onboarding steps using the available tools:
|
||||
|
||||
1. Create a new user account using first_name "{$firstName}" and last_name "{$lastName}" along with the details provided above. Ask for any missing required fields (username and, optionally, email address) before proceeding. Do not ask for a password — one will be set automatically.
|
||||
2. If the new account has an email address, ask whether you should send them a password reset link so they can set their own password. Use send_password_reset if the answer is yes.
|
||||
3. Search for available (undeployed) assets suitable for their role — typically a laptop and any other standard equipment for their department or location.
|
||||
4. Check out the selected assets to the new user.
|
||||
5. Check whether any software license seats are available that should be assigned (e.g. productivity suites, VPN, etc.) and assign them.
|
||||
6. Summarise what was set up: the user account created, whether a password reset email was sent, assets checked out, and licenses assigned.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('first_name', 'First name of the new employee', required: true),
|
||||
new Argument('last_name', 'Last name of the new employee', required: false),
|
||||
new Argument('department', 'Department the employee will join', required: false),
|
||||
new Argument('location', 'Primary office location', required: false),
|
||||
new Argument('title', 'Job title', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
|
||||
abstract class SnipePrompt extends Prompt
|
||||
{
|
||||
/**
|
||||
* Returns a trailing instruction telling the model which language to respond in,
|
||||
* derived from the authenticated user's locale setting. Returns an empty string
|
||||
* for English locales so the prompt text is unchanged for the majority of users.
|
||||
*/
|
||||
protected function localeInstruction(): string
|
||||
{
|
||||
$locale = auth()->user()?->locale ?? app()->getLocale();
|
||||
|
||||
if (str_starts_with($locale, 'en')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return "\n\nPlease respond in the language that corresponds to locale: {$locale}.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('user_inventory')]
|
||||
#[Title('User Inventory')]
|
||||
#[Description('List everything currently assigned to a specific user across all asset types')]
|
||||
class UserInventoryPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$username = $request->get('username');
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are pulling a complete inventory of everything assigned to the user: {$username}.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. Look up the user account for {$username} and display their basic info (name, department, location, job title).
|
||||
2. List all assets currently checked out to this user (asset tag, name, model, serial, status).
|
||||
3. List all accessories checked out to this user.
|
||||
4. List all license seats assigned to this user.
|
||||
5. List any consumables that have been checked out to this user.
|
||||
6. Calculate the total purchase value of all assigned assets if cost data is available.
|
||||
7. Present a clean summary grouped by item type, suitable for sharing with a manager or for an audit.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('username', 'Username of the user to review', required: true),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('warranty_expiring')]
|
||||
#[Title('Warranty Expiring')]
|
||||
#[Description('List assets whose warranty expires within a given number of days')]
|
||||
class WarrantyExpiringPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$days = (int) ($request->get('days', 90));
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are reviewing assets whose warranty is expiring soon. Focus on assets expiring within {$days} days.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. List assets and filter for those whose warranty expiration date (calculated from purchase_date + warranty_months) falls within the next {$days} days.
|
||||
2. For each asset, show: asset tag, name, model, assigned user or location, purchase date, warranty months, and calculated warranty end date.
|
||||
3. Group by urgency: expiring within 30 days, 31–60 days, and 61–{$days} days.
|
||||
4. Flag any assets that are deployed to critical roles or users where warranty coverage is especially important.
|
||||
5. Recommend actions: extend warranty, schedule replacement, or note as acceptable risk.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('days', 'Number of days ahead to check for warranty expiry (default: 90)', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
+1066
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Servers;
|
||||
|
||||
use App\Mcp\Prompts\AuditLocationPrompt;
|
||||
use App\Mcp\Prompts\EndOfLifeReviewPrompt;
|
||||
use App\Mcp\Prompts\ExpiringLicensesPrompt;
|
||||
use App\Mcp\Prompts\FindAvailableAssetPrompt;
|
||||
use App\Mcp\Prompts\InventorySummaryPrompt;
|
||||
use App\Mcp\Prompts\OffboardEmployeePrompt;
|
||||
use App\Mcp\Prompts\OnboardEmployeePrompt;
|
||||
use App\Mcp\Prompts\UserInventoryPrompt;
|
||||
use App\Mcp\Prompts\WarrantyExpiringPrompt;
|
||||
use App\Mcp\Tools\AddAssetNoteTool;
|
||||
use App\Mcp\Tools\AuditAssetTool;
|
||||
use App\Mcp\Tools\CheckinAccessoryTool;
|
||||
use App\Mcp\Tools\CheckinAssetTool;
|
||||
use App\Mcp\Tools\CheckinComponentTool;
|
||||
use App\Mcp\Tools\CheckinLicenseTool;
|
||||
use App\Mcp\Tools\CheckoutAccessoryTool;
|
||||
use App\Mcp\Tools\CheckoutAssetTool;
|
||||
use App\Mcp\Tools\CheckoutComponentTool;
|
||||
use App\Mcp\Tools\CheckoutConsumableTool;
|
||||
use App\Mcp\Tools\CheckoutLicenseTool;
|
||||
use App\Mcp\Tools\CreateAccessoryTool;
|
||||
use App\Mcp\Tools\CreateAssetModelTool;
|
||||
use App\Mcp\Tools\CreateAssetTool;
|
||||
use App\Mcp\Tools\CreateCategoryTool;
|
||||
use App\Mcp\Tools\CreateCompanyTool;
|
||||
use App\Mcp\Tools\CreateComponentTool;
|
||||
use App\Mcp\Tools\CreateConsumableTool;
|
||||
use App\Mcp\Tools\CreateDepartmentTool;
|
||||
use App\Mcp\Tools\CreateDepreciationTool;
|
||||
use App\Mcp\Tools\CreateGroupTool;
|
||||
use App\Mcp\Tools\CreateLicenseTool;
|
||||
use App\Mcp\Tools\CreateLocationTool;
|
||||
use App\Mcp\Tools\CreateMaintenanceTool;
|
||||
use App\Mcp\Tools\CreateManufacturerTool;
|
||||
use App\Mcp\Tools\CreateStatusLabelTool;
|
||||
use App\Mcp\Tools\CreateSupplierTool;
|
||||
use App\Mcp\Tools\CreateUserTool;
|
||||
use App\Mcp\Tools\DeleteAccessoryTool;
|
||||
use App\Mcp\Tools\DeleteAssetModelTool;
|
||||
use App\Mcp\Tools\DeleteAssetTool;
|
||||
use App\Mcp\Tools\DeleteCategoryTool;
|
||||
use App\Mcp\Tools\DeleteCompanyTool;
|
||||
use App\Mcp\Tools\DeleteComponentTool;
|
||||
use App\Mcp\Tools\DeleteConsumableTool;
|
||||
use App\Mcp\Tools\DeleteDepartmentTool;
|
||||
use App\Mcp\Tools\DeleteDepreciationTool;
|
||||
use App\Mcp\Tools\DeleteGroupTool;
|
||||
use App\Mcp\Tools\DeleteLicenseTool;
|
||||
use App\Mcp\Tools\DeleteLocationTool;
|
||||
use App\Mcp\Tools\DeleteManufacturerTool;
|
||||
use App\Mcp\Tools\DeleteStatusLabelTool;
|
||||
use App\Mcp\Tools\DeleteSupplierTool;
|
||||
use App\Mcp\Tools\DeleteUserTool;
|
||||
use App\Mcp\Tools\GetActivityLogTool;
|
||||
use App\Mcp\Tools\GetCurrentUserTool;
|
||||
use App\Mcp\Tools\GetUserAssetsTool;
|
||||
use App\Mcp\Tools\ListAssetModelsTool;
|
||||
use App\Mcp\Tools\ListAssetNotesTool;
|
||||
use App\Mcp\Tools\ListAssetsTool;
|
||||
use App\Mcp\Tools\ListCategoriesTool;
|
||||
use App\Mcp\Tools\ListCompaniesTool;
|
||||
use App\Mcp\Tools\ListConsumablesTool;
|
||||
use App\Mcp\Tools\ListDepreciationsTool;
|
||||
use App\Mcp\Tools\ListGroupsTool;
|
||||
use App\Mcp\Tools\ListHistoryTool;
|
||||
use App\Mcp\Tools\ListLicensesTool;
|
||||
use App\Mcp\Tools\ListLocationsTool;
|
||||
use App\Mcp\Tools\ListMaintenancesTool;
|
||||
use App\Mcp\Tools\ListManufacturersTool;
|
||||
use App\Mcp\Tools\ListStatusLabelsTool;
|
||||
use App\Mcp\Tools\ListSuppliersTool;
|
||||
use App\Mcp\Tools\ListUploadsTool;
|
||||
use App\Mcp\Tools\ListUsersTool;
|
||||
use App\Mcp\Tools\Reset2FATool;
|
||||
use App\Mcp\Tools\RestoreAssetTool;
|
||||
use App\Mcp\Tools\RestoreUserTool;
|
||||
use App\Mcp\Tools\SendPasswordResetTool;
|
||||
use App\Mcp\Tools\ShowAssetModelTool;
|
||||
use App\Mcp\Tools\ShowAssetTool;
|
||||
use App\Mcp\Tools\ShowCategoryTool;
|
||||
use App\Mcp\Tools\ShowCompanyTool;
|
||||
use App\Mcp\Tools\ShowConsumableTool;
|
||||
use App\Mcp\Tools\ShowDepreciationTool;
|
||||
use App\Mcp\Tools\ShowGroupTool;
|
||||
use App\Mcp\Tools\ShowLicenseTool;
|
||||
use App\Mcp\Tools\ShowLocationTool;
|
||||
use App\Mcp\Tools\ShowManufacturerTool;
|
||||
use App\Mcp\Tools\ShowStatusLabelTool;
|
||||
use App\Mcp\Tools\ShowSupplierTool;
|
||||
use App\Mcp\Tools\ShowUserTool;
|
||||
use App\Mcp\Tools\UpdateAccessoryTool;
|
||||
use App\Mcp\Tools\UpdateAssetModelTool;
|
||||
use App\Mcp\Tools\UpdateAssetTool;
|
||||
use App\Mcp\Tools\UpdateCategoryTool;
|
||||
use App\Mcp\Tools\UpdateCompanyTool;
|
||||
use App\Mcp\Tools\UpdateComponentTool;
|
||||
use App\Mcp\Tools\UpdateConsumableTool;
|
||||
use App\Mcp\Tools\UpdateDepartmentTool;
|
||||
use App\Mcp\Tools\UpdateDepreciationTool;
|
||||
use App\Mcp\Tools\UpdateGroupTool;
|
||||
use App\Mcp\Tools\UpdateLicenseTool;
|
||||
use App\Mcp\Tools\UpdateLocationTool;
|
||||
use App\Mcp\Tools\UpdateManufacturerTool;
|
||||
use App\Mcp\Tools\UpdateProfileTool;
|
||||
use App\Mcp\Tools\UpdateStatusLabelTool;
|
||||
use App\Mcp\Tools\UpdateSupplierTool;
|
||||
use App\Mcp\Tools\UpdateUserTool;
|
||||
use Laravel\Mcp\Server;
|
||||
use Laravel\Mcp\Server\Attributes\Instructions;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Version;
|
||||
|
||||
#[Name('Snipe-IT MCP Server')]
|
||||
#[Version('0.0.1')]
|
||||
#[Instructions('This server allows you to interact with the Snipe-IT asset management database. You can list, view, check out, and check in assets.')]
|
||||
class SnipeMCPServer extends Server
|
||||
{
|
||||
protected array $tools = [
|
||||
// Assets
|
||||
ShowAssetTool::class,
|
||||
ListAssetsTool::class,
|
||||
CreateAssetTool::class,
|
||||
UpdateAssetTool::class,
|
||||
DeleteAssetTool::class,
|
||||
RestoreAssetTool::class,
|
||||
CheckoutAssetTool::class,
|
||||
CheckinAssetTool::class,
|
||||
AuditAssetTool::class,
|
||||
AddAssetNoteTool::class,
|
||||
ListAssetNotesTool::class,
|
||||
|
||||
// Cross-type tools
|
||||
ListUploadsTool::class,
|
||||
ListHistoryTool::class,
|
||||
|
||||
// Users
|
||||
ListUsersTool::class,
|
||||
ShowUserTool::class,
|
||||
CreateUserTool::class,
|
||||
UpdateUserTool::class,
|
||||
DeleteUserTool::class,
|
||||
RestoreUserTool::class,
|
||||
GetCurrentUserTool::class,
|
||||
UpdateProfileTool::class,
|
||||
GetUserAssetsTool::class,
|
||||
Reset2FATool::class,
|
||||
SendPasswordResetTool::class,
|
||||
|
||||
// Accessories
|
||||
CreateAccessoryTool::class,
|
||||
UpdateAccessoryTool::class,
|
||||
DeleteAccessoryTool::class,
|
||||
CheckoutAccessoryTool::class,
|
||||
CheckinAccessoryTool::class,
|
||||
|
||||
// Components
|
||||
CreateComponentTool::class,
|
||||
UpdateComponentTool::class,
|
||||
DeleteComponentTool::class,
|
||||
CheckoutComponentTool::class,
|
||||
CheckinComponentTool::class,
|
||||
|
||||
// Consumables
|
||||
ListConsumablesTool::class,
|
||||
ShowConsumableTool::class,
|
||||
CreateConsumableTool::class,
|
||||
UpdateConsumableTool::class,
|
||||
DeleteConsumableTool::class,
|
||||
CheckoutConsumableTool::class,
|
||||
|
||||
// Licenses
|
||||
ListLicensesTool::class,
|
||||
ShowLicenseTool::class,
|
||||
CreateLicenseTool::class,
|
||||
UpdateLicenseTool::class,
|
||||
DeleteLicenseTool::class,
|
||||
CheckoutLicenseTool::class,
|
||||
CheckinLicenseTool::class,
|
||||
|
||||
// Departments
|
||||
CreateDepartmentTool::class,
|
||||
UpdateDepartmentTool::class,
|
||||
DeleteDepartmentTool::class,
|
||||
|
||||
// Companies
|
||||
ListCompaniesTool::class,
|
||||
ShowCompanyTool::class,
|
||||
CreateCompanyTool::class,
|
||||
UpdateCompanyTool::class,
|
||||
DeleteCompanyTool::class,
|
||||
|
||||
// Categories
|
||||
ListCategoriesTool::class,
|
||||
ShowCategoryTool::class,
|
||||
CreateCategoryTool::class,
|
||||
UpdateCategoryTool::class,
|
||||
DeleteCategoryTool::class,
|
||||
|
||||
// Manufacturers
|
||||
ListManufacturersTool::class,
|
||||
ShowManufacturerTool::class,
|
||||
CreateManufacturerTool::class,
|
||||
UpdateManufacturerTool::class,
|
||||
DeleteManufacturerTool::class,
|
||||
|
||||
// Suppliers
|
||||
ListSuppliersTool::class,
|
||||
ShowSupplierTool::class,
|
||||
CreateSupplierTool::class,
|
||||
UpdateSupplierTool::class,
|
||||
DeleteSupplierTool::class,
|
||||
|
||||
// Status Labels
|
||||
ListStatusLabelsTool::class,
|
||||
ShowStatusLabelTool::class,
|
||||
CreateStatusLabelTool::class,
|
||||
UpdateStatusLabelTool::class,
|
||||
DeleteStatusLabelTool::class,
|
||||
|
||||
// Locations
|
||||
ListLocationsTool::class,
|
||||
ShowLocationTool::class,
|
||||
CreateLocationTool::class,
|
||||
UpdateLocationTool::class,
|
||||
DeleteLocationTool::class,
|
||||
|
||||
// Asset Models
|
||||
ListAssetModelsTool::class,
|
||||
ShowAssetModelTool::class,
|
||||
CreateAssetModelTool::class,
|
||||
UpdateAssetModelTool::class,
|
||||
DeleteAssetModelTool::class,
|
||||
|
||||
// Depreciations
|
||||
ListDepreciationsTool::class,
|
||||
ShowDepreciationTool::class,
|
||||
CreateDepreciationTool::class,
|
||||
UpdateDepreciationTool::class,
|
||||
DeleteDepreciationTool::class,
|
||||
|
||||
// Groups
|
||||
ListGroupsTool::class,
|
||||
ShowGroupTool::class,
|
||||
CreateGroupTool::class,
|
||||
UpdateGroupTool::class,
|
||||
DeleteGroupTool::class,
|
||||
|
||||
// Maintenance
|
||||
ListMaintenancesTool::class,
|
||||
CreateMaintenanceTool::class,
|
||||
|
||||
// Activity Log
|
||||
GetActivityLogTool::class,
|
||||
];
|
||||
|
||||
protected array $resources = [
|
||||
//
|
||||
];
|
||||
|
||||
protected array $prompts = [
|
||||
OnboardEmployeePrompt::class,
|
||||
OffboardEmployeePrompt::class,
|
||||
AuditLocationPrompt::class,
|
||||
FindAvailableAssetPrompt::class,
|
||||
ExpiringLicensesPrompt::class,
|
||||
EndOfLifeReviewPrompt::class,
|
||||
WarrantyExpiringPrompt::class,
|
||||
InventorySummaryPrompt::class,
|
||||
UserInventoryPrompt::class,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('add_asset_note')]
|
||||
#[Title('Add Asset Note')]
|
||||
#[Description('Add a manual note to a Snipe-IT asset identified by asset tag, serial number, or numeric ID')]
|
||||
class AddAssetNoteTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|string|max:100',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'id' => 'nullable|integer',
|
||||
'note' => 'required|string|max:50000',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('update', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$logaction = new Actionlog;
|
||||
$logaction->item_type = Asset::class;
|
||||
$logaction->item_id = $asset->id;
|
||||
$logaction->note = $request->get('note');
|
||||
$logaction->created_by = auth()->id();
|
||||
|
||||
if ($logaction->logaction('note added')) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.note_added_to_asset', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.note_added_successfully'),
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'asset_id' => $asset->id,
|
||||
'note' => $logaction->note,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.note_save_failed')));
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))->first();
|
||||
}
|
||||
if ($request->filled('serial')) {
|
||||
return Asset::where('serial', $request->get('serial'))->first();
|
||||
}
|
||||
if ($request->filled('id')) {
|
||||
return Asset::find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset'),
|
||||
'serial' => $schema->string()->description('Serial number of the asset'),
|
||||
'id' => $schema->number()->description('Numeric ID of the asset'),
|
||||
'note' => $schema->string()->description('Note text to add to the asset'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the note was saved'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset'),
|
||||
'asset_id' => $schema->number()->description('Numeric ID of the asset'),
|
||||
'note' => $schema->string()->description('The note that was saved'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Setting;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('audit_asset')]
|
||||
#[Title('Audit Asset')]
|
||||
#[Description('Record an audit for a Snipe-IT asset, updating the last audit date and optionally the location')]
|
||||
class AuditAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'id' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:1000',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'next_audit_date' => 'nullable|date',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('audit', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$originalValues = $asset->getRawOriginal();
|
||||
$settings = Setting::getSettings();
|
||||
|
||||
$asset->last_audit_date = date('Y-m-d H:i:s');
|
||||
|
||||
if ($request->filled('next_audit_date')) {
|
||||
$asset->next_audit_date = $request->get('next_audit_date');
|
||||
} elseif (! is_null($settings->audit_interval)) {
|
||||
$asset->next_audit_date = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$asset->location_id = $request->get('location_id');
|
||||
}
|
||||
|
||||
// Bypass the observer to avoid logging a spurious asset-update entry
|
||||
// alongside the audit log entry created by logAudit() below
|
||||
$asset->unsetEventDispatcher();
|
||||
|
||||
if ($asset->isValid() && $asset->save()) {
|
||||
$asset->logAudit($request->get('note'), $request->get('location_id'), null, $originalValues);
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_audited', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_audited', ['asset_tag' => $asset->asset_tag]),
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'last_audit_date' => $asset->last_audit_date,
|
||||
'next_audit_date' => $asset->next_audit_date,
|
||||
'location' => $asset->location?->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.audit_failed', ['error' => $asset->getErrors()->first()])));
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))->with('location')->first();
|
||||
}
|
||||
if ($request->filled('serial')) {
|
||||
return Asset::where('serial', $request->get('serial'))->with('location')->first();
|
||||
}
|
||||
if ($request->filled('id')) {
|
||||
return Asset::with('location')->find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset to audit'),
|
||||
'serial' => $schema->string()->description('Serial number of the asset to audit'),
|
||||
'id' => $schema->number()->description('Numeric ID of the asset to audit'),
|
||||
'note' => $schema->string()->description('Optional audit note'),
|
||||
'location_id' => $schema->number()->description('Location ID where the asset was found (also updates the asset location)'),
|
||||
'next_audit_date' => $schema->string()->description('Override the next audit date (YYYY-MM-DD); defaults to now plus the audit_interval from settings'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the audit succeeded'),
|
||||
'error' => $schema->boolean()->description('True if the audit failed'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the audited asset'),
|
||||
'last_audit_date' => $schema->string()->description('Timestamp of the audit just recorded'),
|
||||
'next_audit_date' => $schema->string()->description('Date of the next scheduled audit'),
|
||||
'location' => $schema->string()->description('Location name where the asset was found'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\AccessoryCheckout;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkin_accessory')]
|
||||
#[Title('Checkin Accessory')]
|
||||
#[Description('Check in a Snipe-IT accessory checkout record by its checkout ID')]
|
||||
class CheckinAccessoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'checkout_id' => 'required|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$checkout = AccessoryCheckout::find($request->get('checkout_id'));
|
||||
|
||||
if (! $checkout) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_checkout_not_found')));
|
||||
}
|
||||
|
||||
$accessory = Accessory::find($checkout->accessory_id);
|
||||
|
||||
if (! $accessory) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkin', $accessory)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$target = $checkout->assigned_type && $checkout->assigned_to
|
||||
? $checkout->assigned_type::find($checkout->assigned_to)
|
||||
: null;
|
||||
|
||||
$accessory->logCheckin($target, $request->get('note'));
|
||||
|
||||
if ($checkout->delete()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.accessory_checked_in', ['name' => $accessory->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.accessory_checked_in', ['name' => $accessory->name]),
|
||||
'accessory_id' => $accessory->id,
|
||||
'accessory_name' => $accessory->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkin_failed')));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'checkout_id' => $schema->number()->description('ID of the checkout record to check in (returned by checkout_accessory)'),
|
||||
'note' => $schema->string()->description('Optional checkin note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkin succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'accessory_id' => $schema->number()->description('Numeric ID of the accessory'),
|
||||
'accessory_name' => $schema->string()->description('Name of the accessory'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkin_asset')]
|
||||
#[Title('Check In Asset')]
|
||||
#[Description('Check a currently checked-out Snipe-IT asset back in')]
|
||||
class CheckinAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'id' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:1000',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkin', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$target = $asset->assignedTo;
|
||||
|
||||
if (is_null($target)) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_checked_out', ['asset_tag' => $asset->asset_tag])));
|
||||
}
|
||||
|
||||
$originalValues = $asset->getRawOriginal();
|
||||
$checkinAt = date('Y-m-d H:i:s');
|
||||
|
||||
$asset->expected_checkin = null;
|
||||
$asset->last_checkin = now();
|
||||
$asset->assignedTo()->disassociate($asset);
|
||||
$asset->accepted = null;
|
||||
$asset->location_id = $asset->rtd_location_id;
|
||||
|
||||
if ($asset->save()) {
|
||||
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->get('note'), $checkinAt, $originalValues));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_checked_in', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_checked_in', ['asset_tag' => $asset->asset_tag]),
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'model' => $asset->model?->name,
|
||||
'location' => $asset->location?->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkin_failed_error', ['error' => $asset->getErrors()->first()])));
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))
|
||||
->with('model', 'location')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($request->filled('id')) {
|
||||
return Asset::with('model', 'location')->find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()
|
||||
->description('Asset tag of the asset to check in'),
|
||||
'id' => $schema->number()
|
||||
->description('Numeric ID of the asset to check in'),
|
||||
'note' => $schema->string()
|
||||
->description('Optional note to attach to this checkin'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->string()->description('True if the checkin succeeded'),
|
||||
'error' => $schema->string()->description('True if the checkin failed'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the checked-in asset'),
|
||||
'model' => $schema->string()->description('Model name of the checked-in asset'),
|
||||
'location' => $schema->string()->description('Location the asset returned to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Component;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkin_component')]
|
||||
#[Title('Checkin Component')]
|
||||
#[Description('Check in one or more units of a Snipe-IT component from an asset using the checkout record ID')]
|
||||
class CheckinComponentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'component_asset_id' => 'required|integer',
|
||||
'checkin_qty' => 'nullable|integer|min:1',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$componentAsset = DB::table('components_assets')->find($request->get('component_asset_id'));
|
||||
|
||||
if (! $componentAsset) {
|
||||
return Response::make(Response::error(trans('mcp.component_checkout_not_found')));
|
||||
}
|
||||
|
||||
$component = Component::find($componentAsset->component_id);
|
||||
|
||||
if (! $component) {
|
||||
return Response::make(Response::error(trans('mcp.component_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkin', $component)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$maxCheckin = $componentAsset->assigned_qty ?? 1;
|
||||
$checkinQty = (int) $request->get('checkin_qty', $maxCheckin);
|
||||
|
||||
if ($checkinQty > $maxCheckin) {
|
||||
return Response::make(Response::error(
|
||||
'Checkin quantity ('.$checkinQty.') exceeds assigned quantity ('.$maxCheckin.')'
|
||||
));
|
||||
}
|
||||
|
||||
$remaining = $maxCheckin - $checkinQty;
|
||||
|
||||
if ($remaining === 0) {
|
||||
DB::table('components_assets')->where('id', $componentAsset->id)->delete();
|
||||
} else {
|
||||
DB::table('components_assets')->where('id', $componentAsset->id)->update(['assigned_qty' => $remaining]);
|
||||
}
|
||||
|
||||
$asset = Asset::find($componentAsset->asset_id);
|
||||
|
||||
event(new CheckoutableCheckedIn($component, $asset, auth()->user(), $request->get('note'), Carbon::now()));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.component_checked_in', ['name' => $component->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.component_checked_in', ['name' => $component->name]),
|
||||
'component_id' => $component->id,
|
||||
'component_name' => $component->name,
|
||||
'checkin_qty' => $checkinQty,
|
||||
'qty_still_checked_out' => $remaining,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'component_asset_id' => $schema->number()->description('ID of the checkout record to check in (returned by checkout_component)'),
|
||||
'checkin_qty' => $schema->number()->description('Number of units to check in (default: all assigned units)'),
|
||||
'note' => $schema->string()->description('Optional checkin note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkin succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'component_id' => $schema->number()->description('Numeric ID of the component'),
|
||||
'component_name' => $schema->string()->description('Name of the component'),
|
||||
'checkin_qty' => $schema->number()->description('Number of units checked in'),
|
||||
'qty_still_checked_out' => $schema->number()->description('Units remaining checked out on this record (0 means fully returned)'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkin_license')]
|
||||
#[Title('Checkin License')]
|
||||
#[Description('Check in a Snipe-IT license seat by its seat ID, returning it to the available pool')]
|
||||
class CheckinLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'seat_id' => 'required|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$seat = LicenseSeat::with('license')->find($request->get('seat_id'));
|
||||
|
||||
if (! $seat) {
|
||||
return Response::make(Response::error(trans('mcp.license_seat_not_found')));
|
||||
}
|
||||
|
||||
if (is_null($seat->assigned_to) && is_null($seat->asset_id)) {
|
||||
return Response::make(Response::error(trans('mcp.seat_not_checked_out')));
|
||||
}
|
||||
|
||||
$license = $seat->license;
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error(trans('mcp.license_not_found')));
|
||||
}
|
||||
|
||||
// License checkin uses the checkout gate (matching application behavior)
|
||||
if (! Gate::allows('checkout', $license)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$returnTo = null;
|
||||
if ($seat->assigned_to) {
|
||||
$returnTo = User::withTrashed()->find($seat->assigned_to);
|
||||
} elseif ($seat->asset_id) {
|
||||
$returnTo = Asset::find($seat->asset_id);
|
||||
}
|
||||
|
||||
$note = $request->get('note');
|
||||
|
||||
$seat->assigned_to = null;
|
||||
$seat->asset_id = null;
|
||||
$seat->notes = $note;
|
||||
|
||||
if (! $license->reassignable) {
|
||||
$seat->unreassignable_seat = true;
|
||||
}
|
||||
|
||||
if ($seat->save()) {
|
||||
event(new CheckoutableCheckedIn($seat, $returnTo, auth()->user(), $note));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_seat_checked_in', ['id' => $seat->id]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_seat_checked_in', ['id' => $seat->id]),
|
||||
'seat_id' => $seat->id,
|
||||
'license_id' => $license->id,
|
||||
'license_name' => $license->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkin_failed')));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'seat_id' => $schema->number()->description('ID of the license seat to check in (returned by checkout_license)'),
|
||||
'note' => $schema->string()->description('Optional checkin note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkin succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'seat_id' => $schema->number()->description('ID of the seat that was checked in'),
|
||||
'license_id' => $schema->number()->description('Numeric ID of the license'),
|
||||
'license_name' => $schema->string()->description('Name of the license'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\AccessoryCheckout;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Location;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_accessory')]
|
||||
#[Title('Checkout Accessory')]
|
||||
#[Description('Check out a Snipe-IT accessory to a user, location, or asset')]
|
||||
class CheckoutAccessoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'checkout_to_type' => 'required|in:user,location,asset',
|
||||
'assigned_user' => 'nullable|integer',
|
||||
'assigned_location' => 'nullable|integer',
|
||||
'assigned_asset' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$accessory = $this->resolveAccessory($request);
|
||||
|
||||
if (! $accessory) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $accessory)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($accessory->numRemaining() < 1) {
|
||||
return Response::make(Response::error(trans('mcp.no_units_available')));
|
||||
}
|
||||
|
||||
$checkoutType = $request->get('checkout_to_type');
|
||||
|
||||
$target = match ($checkoutType) {
|
||||
'user' => User::find($request->get('assigned_user')),
|
||||
'location' => Location::find($request->get('assigned_location')),
|
||||
'asset' => Asset::find($request->get('assigned_asset')),
|
||||
};
|
||||
|
||||
if (! $target) {
|
||||
return Response::make(Response::error(trans('mcp.checkout_target_not_found', ['type' => $checkoutType])));
|
||||
}
|
||||
|
||||
$checkout = new AccessoryCheckout([
|
||||
'accessory_id' => $accessory->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_to' => $target->id,
|
||||
'assigned_type' => $target::class,
|
||||
'note' => $request->get('note'),
|
||||
]);
|
||||
$checkout->created_by = auth()->id();
|
||||
$checkout->save();
|
||||
|
||||
event(new CheckoutableCheckedOut(
|
||||
$accessory,
|
||||
$target,
|
||||
auth()->user(),
|
||||
$request->get('note'),
|
||||
[],
|
||||
1,
|
||||
));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.accessory_checked_out', ['name' => $accessory->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.accessory_checked_out', ['name' => $accessory->name]),
|
||||
'accessory_id' => $accessory->id,
|
||||
'accessory_name' => $accessory->name,
|
||||
'checkout_id' => $checkout->id,
|
||||
'checked_out_to_type' => $checkoutType,
|
||||
'checked_out_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAccessory(Request $request): ?Accessory
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Accessory::withCount('checkouts as checkouts_count')->find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Accessory::withCount('checkouts as checkouts_count')->where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the accessory to check out'),
|
||||
'name' => $schema->string()->description('Name of the accessory to check out'),
|
||||
'checkout_to_type' => $schema->string()->description('Target type: user, location, or asset (required)'),
|
||||
'assigned_user' => $schema->number()->description('User ID to check out to'),
|
||||
'assigned_location' => $schema->number()->description('Location ID to check out to'),
|
||||
'assigned_asset' => $schema->number()->description('Asset ID to check out to'),
|
||||
'note' => $schema->string()->description('Optional checkout note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkout succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'accessory_id' => $schema->number()->description('Numeric ID of the accessory'),
|
||||
'accessory_name' => $schema->string()->description('Name of the accessory'),
|
||||
'checkout_id' => $schema->number()->description('ID of the checkout record (use this for checkin)'),
|
||||
'checked_out_to_type' => $schema->string()->description('Type of target: user, location, or asset'),
|
||||
'checked_out_to_id' => $schema->number()->description('ID of the target'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Location;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_asset')]
|
||||
#[Title('Checkout Asset')]
|
||||
#[Description('Check out a Snipe-IT asset to a user, location, or another asset')]
|
||||
class CheckoutAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'id' => 'nullable|integer',
|
||||
'checkout_to_type' => 'required|string|in:user,location,asset',
|
||||
'assigned_user' => 'nullable|integer',
|
||||
'assigned_location' => 'nullable|integer',
|
||||
'assigned_asset' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:1000',
|
||||
'checkout_at' => 'nullable|date',
|
||||
'expected_checkin' => 'nullable|date',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if (! $asset->availableForCheckout()) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_available', ['asset_tag' => $asset->asset_tag])));
|
||||
}
|
||||
|
||||
$checkoutType = $request->get('checkout_to_type');
|
||||
$target = null;
|
||||
|
||||
if ($checkoutType === 'user') {
|
||||
$target = User::find($request->get('assigned_user'));
|
||||
if ($target) {
|
||||
$asset->location_id = $target->location_id ?? $asset->location_id;
|
||||
}
|
||||
} elseif ($checkoutType === 'location') {
|
||||
$target = Location::find($request->get('assigned_location'));
|
||||
if ($target) {
|
||||
$asset->location_id = $target->id;
|
||||
}
|
||||
} elseif ($checkoutType === 'asset') {
|
||||
$target = Asset::where('id', '!=', $asset->id)->find($request->get('assigned_asset'));
|
||||
if ($target) {
|
||||
$asset->location_id = $target->location_id ?? $asset->location_id;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $target) {
|
||||
return Response::make(Response::error(trans('mcp.checkout_target_not_found', ['type' => $checkoutType])));
|
||||
}
|
||||
|
||||
$checkoutAt = $request->filled('checkout_at') ? $request->get('checkout_at') : date('Y-m-d H:i:s');
|
||||
$expectedCheckin = $request->filled('expected_checkin') ? $request->get('expected_checkin') : null;
|
||||
$note = $request->filled('note') ? $request->get('note') : null;
|
||||
|
||||
if ($asset->checkOut($target, auth()->user(), $checkoutAt, $expectedCheckin, $note, $asset->name, $asset->location_id)) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_checked_out', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_checked_out', ['asset_tag' => $asset->asset_tag]),
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'checked_out_to_type' => $checkoutType,
|
||||
'checked_out_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkout_failed')));
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))
|
||||
->with('status')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($request->filled('id')) {
|
||||
return Asset::with('status')->find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()
|
||||
->description('Asset tag of the asset to check out'),
|
||||
'id' => $schema->number()
|
||||
->description('Numeric ID of the asset to check out'),
|
||||
'checkout_to_type' => $schema->string()
|
||||
->description('What to check the asset out to: user, location, or asset')
|
||||
->required(),
|
||||
'assigned_user' => $schema->number()
|
||||
->description('ID of the user to check the asset out to (when checkout_to_type is user)'),
|
||||
'assigned_location' => $schema->number()
|
||||
->description('ID of the location to check the asset out to (when checkout_to_type is location)'),
|
||||
'assigned_asset' => $schema->number()
|
||||
->description('ID of the asset to check the asset out to (when checkout_to_type is asset)'),
|
||||
'note' => $schema->string()
|
||||
->description('Optional note to attach to this checkout'),
|
||||
'checkout_at' => $schema->string()
|
||||
->description('Checkout date/time (defaults to now, format: YYYY-MM-DD)'),
|
||||
'expected_checkin' => $schema->string()
|
||||
->description('Expected checkin date (format: YYYY-MM-DD)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->string()->description('True if the checkout succeeded'),
|
||||
'error' => $schema->string()->description('True if the checkout failed'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the checked-out asset'),
|
||||
'checked_out_to_type' => $schema->string()->description('Type of entity the asset was checked out to'),
|
||||
'checked_out_to_id' => $schema->number()->description('ID of the entity the asset was checked out to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Component;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_component')]
|
||||
#[Title('Checkout Component')]
|
||||
#[Description('Check out one or more units of a Snipe-IT component to an asset')]
|
||||
class CheckoutComponentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:191',
|
||||
'asset_id' => 'required|integer|exists:assets,id',
|
||||
'assigned_qty' => 'nullable|integer|min:1',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$component = $this->resolveComponent($request);
|
||||
|
||||
if (! $component) {
|
||||
return Response::make(Response::error(trans('mcp.component_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $component)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$qty = (int) $request->get('assigned_qty', 1);
|
||||
|
||||
if ($component->numRemaining() < $qty) {
|
||||
return Response::make(Response::error(
|
||||
'Not enough units available. Requested: '.$qty.', remaining: '.$component->numRemaining()
|
||||
));
|
||||
}
|
||||
|
||||
$asset = Asset::find($request->get('asset_id'));
|
||||
|
||||
$component->assets()->attach($component->id, [
|
||||
'component_id' => $component->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_qty' => $qty,
|
||||
'created_by' => auth()->id(),
|
||||
'asset_id' => $asset->id,
|
||||
'note' => $request->get('note'),
|
||||
]);
|
||||
|
||||
$pivotId = $component->assets()->wherePivot('asset_id', $asset->id)->latest('components_assets.created_at')->first()?->pivot->id;
|
||||
|
||||
$component->logCheckout($request->get('note'), $asset, null, [], $qty);
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.component_checked_out', ['name' => $component->name, 'asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.component_checked_out', ['name' => $component->name, 'asset_tag' => $asset->asset_tag]),
|
||||
'component_id' => $component->id,
|
||||
'component_name' => $component->name,
|
||||
'asset_id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'assigned_qty' => $qty,
|
||||
'component_asset_id' => $pivotId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveComponent(Request $request): ?Component
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Component::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Component::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the component to check out'),
|
||||
'name' => $schema->string()->description('Name of the component to check out'),
|
||||
'asset_id' => $schema->number()->description('Asset ID to check the component out to (required)'),
|
||||
'assigned_qty' => $schema->number()->description('Number of units to check out (default: 1)'),
|
||||
'note' => $schema->string()->description('Optional checkout note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkout succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'component_id' => $schema->number()->description('Numeric ID of the component'),
|
||||
'component_name' => $schema->string()->description('Name of the component'),
|
||||
'asset_id' => $schema->number()->description('ID of the asset checked out to'),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset checked out to'),
|
||||
'assigned_qty' => $schema->number()->description('Number of units checked out'),
|
||||
'component_asset_id' => $schema->number()->description('ID of the checkout record (use this for checkin)'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_consumable')]
|
||||
#[Title('Checkout Consumable')]
|
||||
#[Description('Check out a Snipe-IT consumable to a user')]
|
||||
class CheckoutConsumableTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'assigned_to' => 'required|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$consumable = $this->resolveConsumable($request);
|
||||
|
||||
if (! $consumable) {
|
||||
return Response::make(Response::error(trans('mcp.consumable_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $consumable)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($consumable->numRemaining() <= 0) {
|
||||
return Response::make(Response::error(trans('mcp.no_units_remaining')));
|
||||
}
|
||||
|
||||
$user = User::find($request->get('assigned_to'));
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
|
||||
$consumable->users()->attach($consumable->id, [
|
||||
'consumable_id' => $consumable->id,
|
||||
'created_by' => auth()->id(),
|
||||
'assigned_to' => $user->id,
|
||||
'note' => $request->get('note'),
|
||||
]);
|
||||
|
||||
event(new CheckoutableCheckedOut(
|
||||
$consumable,
|
||||
$user,
|
||||
auth()->user(),
|
||||
$request->get('note'),
|
||||
[],
|
||||
1,
|
||||
));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.consumable_checked_out', ['name' => $consumable->name, 'username' => $user->username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.consumable_checked_out', ['name' => $consumable->name, 'username' => $user->username]),
|
||||
'consumable_id' => $consumable->id,
|
||||
'consumable_name' => $consumable->name,
|
||||
'assigned_to_id' => $user->id,
|
||||
'assigned_to_username' => $user->username,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveConsumable(Request $request): ?Consumable
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Consumable::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Consumable::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the consumable to check out'),
|
||||
'name' => $schema->string()->description('Name of the consumable to check out'),
|
||||
'assigned_to' => $schema->number()->description('User ID to check out to (required)'),
|
||||
'note' => $schema->string()->description('Optional checkout note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkout succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'consumable_id' => $schema->number()->description('Numeric ID of the consumable'),
|
||||
'consumable_name' => $schema->string()->description('Name of the consumable'),
|
||||
'assigned_to_id' => $schema->number()->description('ID of the user the consumable was checked out to'),
|
||||
'assigned_to_username' => $schema->string()->description('Username of the user the consumable was checked out to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Models\Asset;
|
||||
use App\Models\License;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_license')]
|
||||
#[Title('Checkout License')]
|
||||
#[Description('Check out an available license seat to a user or asset')]
|
||||
class CheckoutLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'assigned_to' => 'nullable|integer',
|
||||
'asset_id' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$license = $this->resolveLicense($request);
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error(trans('mcp.license_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $license)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($license->numRemaining() < 1) {
|
||||
return Response::make(Response::error(trans('mcp.no_available_seats')));
|
||||
}
|
||||
|
||||
if (! $request->filled('assigned_to') && ! $request->filled('asset_id')) {
|
||||
return Response::make(Response::error(trans('mcp.provide_user_or_asset')));
|
||||
}
|
||||
|
||||
$seat = $license->freeSeat();
|
||||
|
||||
if (! $seat) {
|
||||
return Response::make(Response::error(trans('mcp.no_free_seat')));
|
||||
}
|
||||
|
||||
$note = $request->get('note');
|
||||
|
||||
if ($request->filled('assigned_to')) {
|
||||
$target = User::find($request->get('assigned_to'));
|
||||
if (! $target) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
$seat->assigned_to = $target->id;
|
||||
$seat->notes = $note;
|
||||
|
||||
if ($seat->save()) {
|
||||
event(new CheckoutableCheckedOut($seat, $target, auth()->user(), $note, [], 1));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_seat_checked_out_user', ['username' => $target->username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_seat_checked_out_user', ['username' => $target->username]),
|
||||
'license_id' => $license->id,
|
||||
'license_name' => $license->name,
|
||||
'seat_id' => $seat->id,
|
||||
'assigned_to_type' => 'user',
|
||||
'assigned_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
} elseif ($request->filled('asset_id')) {
|
||||
$target = Asset::find($request->get('asset_id'));
|
||||
if (! $target) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
$seat->asset_id = $target->id;
|
||||
if ($target->checkedOutToUser()) {
|
||||
$seat->assigned_to = $target->assigned_to;
|
||||
}
|
||||
$seat->notes = $note;
|
||||
|
||||
if ($seat->save()) {
|
||||
event(new CheckoutableCheckedOut($seat, $target, auth()->user(), $note, [], 1));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_seat_checked_out_asset', ['asset_tag' => $target->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_seat_checked_out_asset', ['asset_tag' => $target->asset_tag]),
|
||||
'license_id' => $license->id,
|
||||
'license_name' => $license->name,
|
||||
'seat_id' => $seat->id,
|
||||
'assigned_to_type' => 'asset',
|
||||
'assigned_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkout_failed')));
|
||||
}
|
||||
|
||||
private function resolveLicense(Request $request): ?License
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return License::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return License::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the license to check out'),
|
||||
'name' => $schema->string()->description('Name of the license to check out'),
|
||||
'assigned_to' => $schema->number()->description('User ID to assign the seat to'),
|
||||
'asset_id' => $schema->number()->description('Asset ID to assign the seat to'),
|
||||
'note' => $schema->string()->description('Optional checkout note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkout succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'license_id' => $schema->number()->description('Numeric ID of the license'),
|
||||
'license_name' => $schema->string()->description('Name of the license'),
|
||||
'seat_id' => $schema->number()->description('ID of the seat record (use this for checkin)'),
|
||||
'assigned_to_type' => $schema->string()->description('Type of entity checked out to: user or asset'),
|
||||
'assigned_to_id' => $schema->number()->description('ID of the entity checked out to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Company;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_accessory')]
|
||||
#[Title('Create Accessory')]
|
||||
#[Description('Create a new Snipe-IT accessory')]
|
||||
class CreateAccessoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Accessory::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'qty' => 'nullable|integer|min:0',
|
||||
'model_number' => 'nullable|string|max:255',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'order_number' => 'nullable|string|max:255',
|
||||
'purchase_cost' => 'nullable|numeric|min:0',
|
||||
'purchase_date' => 'nullable|date_format:Y-m-d',
|
||||
'min_amt' => 'nullable|integer|min:0',
|
||||
'requestable' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$accessory = new Accessory;
|
||||
$accessory->fill($request->only([
|
||||
'name', 'category_id', 'qty', 'model_number', 'manufacturer_id',
|
||||
'supplier_id', 'location_id', 'order_number', 'purchase_cost',
|
||||
'purchase_date', 'min_amt', 'requestable', 'notes',
|
||||
]));
|
||||
|
||||
$accessory->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$accessory->created_by = auth()->id();
|
||||
|
||||
if ($accessory->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.accessory_created', ['name' => $accessory->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.accessory_created', ['name' => $accessory->name]),
|
||||
'id' => $accessory->id,
|
||||
'name' => $accessory->name,
|
||||
'qty' => $accessory->qty,
|
||||
'category_id' => $accessory->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $accessory->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Accessory name (required)'),
|
||||
'category_id' => $schema->number()->description('Category ID — must be an accessory category (required)'),
|
||||
'qty' => $schema->number()->description('Total quantity in stock'),
|
||||
'model_number' => $schema->string()->description('Model number'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'order_number' => $schema->string()->description('Order number'),
|
||||
'purchase_cost' => $schema->number()->description('Purchase cost per unit'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'min_amt' => $schema->number()->description('Minimum quantity threshold for alerts'),
|
||||
'requestable' => $schema->boolean()->description('Whether users can request this accessory'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the accessory was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new accessory'),
|
||||
'name' => $schema->string()->description('Name of the new accessory'),
|
||||
'qty' => $schema->number()->description('Total quantity'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\AssetModel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_asset_model')]
|
||||
#[Title('Create Asset Model')]
|
||||
#[Description('Create a new Snipe-IT asset model')]
|
||||
class CreateAssetModelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', AssetModel::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'model_number' => 'nullable|string|max:255',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'depreciation_id' => 'nullable|integer|exists:depreciations,id',
|
||||
'eol' => 'nullable|integer|min:0|max:240',
|
||||
'min_amt' => 'nullable|integer|min:0',
|
||||
'notes' => 'nullable|string',
|
||||
'requestable' => 'nullable|boolean',
|
||||
'require_serial' => 'nullable|boolean',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$assetModel = new AssetModel;
|
||||
$assetModel->name = $request->get('name');
|
||||
$assetModel->category_id = $request->get('category_id');
|
||||
$assetModel->created_by = auth()->id();
|
||||
|
||||
foreach (['model_number', 'manufacturer_id', 'depreciation_id', 'eol', 'min_amt', 'notes', 'requestable', 'require_serial'] as $f) {
|
||||
if ($request->filled($f)) {
|
||||
$assetModel->{$f} = $request->get($f);
|
||||
}
|
||||
}
|
||||
|
||||
if ($assetModel->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_model_created', ['name' => $assetModel->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_model_created', ['name' => $assetModel->name]),
|
||||
'id' => $assetModel->id,
|
||||
'name' => $assetModel->name,
|
||||
'category_id' => $assetModel->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $assetModel->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Asset model name (required)'),
|
||||
'category_id' => $schema->number()->description('Category ID (required)'),
|
||||
'model_number' => $schema->string()->description('Model number'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'depreciation_id' => $schema->number()->description('Depreciation schedule ID'),
|
||||
'eol' => $schema->number()->description('End of life in months (0-240)'),
|
||||
'min_amt' => $schema->number()->description('Minimum quantity alert threshold'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'requestable' => $schema->boolean()->description('Whether the model can be requested'),
|
||||
'require_serial' => $schema->boolean()->description('Whether serial numbers are required'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the asset model was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new asset model'),
|
||||
'name' => $schema->string()->description('Name of the new asset model'),
|
||||
'category_id' => $schema->number()->description('Category ID of the new asset model'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_asset')]
|
||||
#[Title('Create Asset')]
|
||||
#[Description('Create a new Snipe-IT asset')]
|
||||
class CreateAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'model_id' => 'required|integer|exists:models,id',
|
||||
'status_id' => 'required|integer|exists:status_labels,id',
|
||||
'asset_tag' => 'required|string|max:255',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'serial' => 'nullable|string',
|
||||
'company_id' => 'nullable|integer',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'rtd_location_id' => 'nullable|integer|exists:locations,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'purchase_date' => 'nullable|date_format:Y-m-d',
|
||||
'purchase_cost' => 'nullable|numeric',
|
||||
'order_number' => 'nullable|string|max:191',
|
||||
'warranty_months' => 'nullable|integer|min:0|max:240',
|
||||
'requestable' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string|max:65535',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$asset = new Asset;
|
||||
$asset->model_id = $request->get('model_id');
|
||||
$asset->status_id = $request->get('status_id');
|
||||
$asset->asset_tag = $request->get('asset_tag');
|
||||
$asset->created_by = auth()->id();
|
||||
|
||||
foreach (['name', 'serial', 'company_id', 'location_id', 'rtd_location_id', 'supplier_id', 'purchase_date', 'purchase_cost', 'order_number', 'warranty_months', 'requestable', 'notes'] as $field) {
|
||||
if ($request->filled($field)) {
|
||||
$asset->{$field} = $request->get($field);
|
||||
}
|
||||
}
|
||||
|
||||
if ($asset->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_created', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_created', ['asset_tag' => $asset->asset_tag]),
|
||||
'id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'name' => $asset->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $asset->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'model_id' => $schema->number()->description('Asset model ID (required)'),
|
||||
'status_id' => $schema->number()->description('Status label ID (required)'),
|
||||
'asset_tag' => $schema->string()->description('Asset tag (required)'),
|
||||
'name' => $schema->string()->description('Display name for the asset'),
|
||||
'serial' => $schema->string()->description('Serial number'),
|
||||
'company_id' => $schema->number()->description('Company ID'),
|
||||
'location_id' => $schema->number()->description('Current location ID'),
|
||||
'rtd_location_id' => $schema->number()->description('Default RTD location ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'purchase_cost' => $schema->number()->description('Purchase cost'),
|
||||
'order_number' => $schema->string()->description('Order number'),
|
||||
'warranty_months' => $schema->number()->description('Warranty length in months (0-240)'),
|
||||
'requestable' => $schema->boolean()->description('Whether the asset is user-requestable'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the asset was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new asset'),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the new asset'),
|
||||
'name' => $schema->string()->description('Display name of the new asset'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_category')]
|
||||
#[Title('Create Category')]
|
||||
#[Description('Create a new Snipe-IT category')]
|
||||
class CreateCategoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Category::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'category_type' => 'required|string|in:asset,accessory,consumable,component,license',
|
||||
'checkin_email' => 'nullable|boolean',
|
||||
'require_acceptance' => 'nullable|boolean',
|
||||
'use_default_eula' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$category = new Category;
|
||||
$category->name = $request->get('name');
|
||||
$category->category_type = $request->get('category_type');
|
||||
$category->created_by = auth()->id();
|
||||
|
||||
foreach (['checkin_email', 'require_acceptance', 'use_default_eula', 'notes'] as $field) {
|
||||
if ($request->filled($field)) {
|
||||
$category->{$field} = $request->get($field);
|
||||
}
|
||||
}
|
||||
|
||||
if ($category->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.category_created', ['name' => $category->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.category_created', ['name' => $category->name]),
|
||||
'id' => $category->id,
|
||||
'name' => $category->name,
|
||||
'category_type' => $category->category_type,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $category->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Category name (required)'),
|
||||
'category_type' => $schema->string()->description('Category type (required): asset, accessory, consumable, component, or license'),
|
||||
'checkin_email' => $schema->boolean()->description('Send checkin email when items are checked in'),
|
||||
'require_acceptance' => $schema->boolean()->description('Require user acceptance when checking out'),
|
||||
'use_default_eula' => $schema->boolean()->description('Use the default EULA'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the category was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new category'),
|
||||
'name' => $schema->string()->description('Name of the new category'),
|
||||
'category_type' => $schema->string()->description('Type of the new category'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_company')]
|
||||
#[Title('Create Company')]
|
||||
#[Description('Create a new Snipe-IT company')]
|
||||
class CreateCompanyTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Company::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'phone' => 'nullable|string',
|
||||
'fax' => 'nullable|string',
|
||||
'email' => 'nullable|string',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$company = new Company;
|
||||
$company->name = $request->get('name');
|
||||
if ($request->filled('phone')) {
|
||||
$company->phone = $request->get('phone');
|
||||
}
|
||||
if ($request->filled('fax')) {
|
||||
$company->fax = $request->get('fax');
|
||||
}
|
||||
if ($request->filled('email')) {
|
||||
$company->email = $request->get('email');
|
||||
}
|
||||
if ($request->filled('notes')) {
|
||||
$company->notes = $request->get('notes');
|
||||
}
|
||||
$company->created_by = auth()->id();
|
||||
|
||||
if ($company->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.company_created', ['name' => $company->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.company_created', ['name' => $company->name]),
|
||||
'id' => $company->id,
|
||||
'name' => $company->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $company->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Company name (required)'),
|
||||
'phone' => $schema->string()->description('Company phone number'),
|
||||
'fax' => $schema->string()->description('Company fax number'),
|
||||
'email' => $schema->string()->description('Company email address'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the company was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new company'),
|
||||
'name' => $schema->string()->description('Name of the new company'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Component;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_component')]
|
||||
#[Title('Create Component')]
|
||||
#[Description('Create a new Snipe-IT component')]
|
||||
class CreateComponentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Component::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:191',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'qty' => 'required|integer|min:1',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'model_number' => 'nullable|string|max:255',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'order_number' => 'nullable|string|max:255',
|
||||
'purchase_cost' => 'nullable|numeric|min:0',
|
||||
'purchase_date' => 'nullable|date_format:Y-m-d',
|
||||
'min_amt' => 'nullable|integer|min:0',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$component = new Component;
|
||||
$component->fill($request->only([
|
||||
'name', 'category_id', 'qty', 'serial', 'model_number',
|
||||
'manufacturer_id', 'supplier_id', 'location_id',
|
||||
'order_number', 'purchase_cost', 'purchase_date', 'min_amt', 'notes',
|
||||
]));
|
||||
|
||||
$component->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$component->created_by = auth()->id();
|
||||
|
||||
if ($component->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.component_created', ['name' => $component->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.component_created', ['name' => $component->name]),
|
||||
'id' => $component->id,
|
||||
'name' => $component->name,
|
||||
'qty' => $component->qty,
|
||||
'category_id' => $component->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $component->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Component name (required)'),
|
||||
'category_id' => $schema->number()->description('Category ID — must be a component category (required)'),
|
||||
'qty' => $schema->number()->description('Total quantity in stock (required, min 1)'),
|
||||
'serial' => $schema->string()->description('Serial number'),
|
||||
'model_number' => $schema->string()->description('Model number'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'order_number' => $schema->string()->description('Order number'),
|
||||
'purchase_cost' => $schema->number()->description('Purchase cost per unit'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'min_amt' => $schema->number()->description('Minimum quantity threshold for alerts'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the component was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new component'),
|
||||
'name' => $schema->string()->description('Name of the new component'),
|
||||
'qty' => $schema->number()->description('Total quantity'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Consumable;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_consumable')]
|
||||
#[Title('Create Consumable')]
|
||||
#[Description('Create a new Snipe-IT consumable')]
|
||||
class CreateConsumableTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Consumable::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'qty' => 'required|integer|min:0',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'company_id' => 'nullable|integer',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'item_no' => 'nullable|string|max:255',
|
||||
'order_number' => 'nullable|string|max:255',
|
||||
'model_number' => 'nullable|string|max:255',
|
||||
'purchase_cost' => 'nullable|numeric|min:0',
|
||||
'purchase_date' => 'nullable|date_format:Y-m-d',
|
||||
'min_amt' => 'nullable|integer|min:0',
|
||||
'requestable' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$consumable = new Consumable;
|
||||
$consumable->fill($request->only([
|
||||
'name', 'qty', 'category_id', 'company_id', 'location_id', 'manufacturer_id',
|
||||
'supplier_id', 'item_no', 'order_number', 'model_number', 'purchase_cost',
|
||||
'purchase_date', 'min_amt', 'requestable', 'notes',
|
||||
]));
|
||||
$consumable->created_by = auth()->id();
|
||||
|
||||
if ($consumable->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.consumable_created', ['name' => $consumable->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.consumable_created', ['name' => $consumable->name]),
|
||||
'id' => $consumable->id,
|
||||
'name' => $consumable->name,
|
||||
'qty' => $consumable->qty,
|
||||
'category_id' => $consumable->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $consumable->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Consumable name (required)'),
|
||||
'qty' => $schema->number()->description('Total quantity in stock (required)'),
|
||||
'category_id' => $schema->number()->description('Category ID — must be a consumable category (required)'),
|
||||
'company_id' => $schema->number()->description('Company ID'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'item_no' => $schema->string()->description('Item number'),
|
||||
'order_number' => $schema->string()->description('Order number'),
|
||||
'model_number' => $schema->string()->description('Model number'),
|
||||
'purchase_cost' => $schema->number()->description('Purchase cost per unit'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'min_amt' => $schema->number()->description('Minimum quantity threshold for alerts'),
|
||||
'requestable' => $schema->boolean()->description('Whether users can request this consumable'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the consumable was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new consumable'),
|
||||
'name' => $schema->string()->description('Name of the new consumable'),
|
||||
'qty' => $schema->number()->description('Total quantity'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Department;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_department')]
|
||||
#[Title('Create Department')]
|
||||
#[Description('Create a new Snipe-IT department')]
|
||||
class CreateDepartmentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Department::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'manager_id' => 'nullable|integer|exists:users,id',
|
||||
'phone' => 'nullable|string|max:255',
|
||||
'fax' => 'nullable|string|max:255',
|
||||
'notes' => 'nullable|string|max:255',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$department = new Department;
|
||||
$department->fill($request->only([
|
||||
'name', 'location_id', 'manager_id', 'phone', 'fax', 'notes',
|
||||
]));
|
||||
|
||||
$department->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$department->created_by = auth()->id();
|
||||
|
||||
if ($department->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.department_created', ['name' => $department->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.department_created', ['name' => $department->name]),
|
||||
'id' => $department->id,
|
||||
'name' => $department->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $department->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Department name (required)'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'manager_id' => $schema->number()->description('User ID of the department manager'),
|
||||
'phone' => $schema->string()->description('Department phone number'),
|
||||
'fax' => $schema->string()->description('Department fax number'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the department was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new department'),
|
||||
'name' => $schema->string()->description('Name of the new department'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Depreciation;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_depreciation')]
|
||||
#[Title('Create Depreciation')]
|
||||
#[Description('Create a new Snipe-IT depreciation schedule')]
|
||||
class CreateDepreciationTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Depreciation::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'months' => 'required|integer|min:1|max:3600',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$depreciation = new Depreciation;
|
||||
$depreciation->name = $request->get('name');
|
||||
$depreciation->months = $request->get('months');
|
||||
|
||||
if ($depreciation->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.depreciation_created', ['name' => $depreciation->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.depreciation_created', ['name' => $depreciation->name]),
|
||||
'id' => $depreciation->id,
|
||||
'name' => $depreciation->name,
|
||||
'months' => $depreciation->months,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $depreciation->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Depreciation name (required)'),
|
||||
'months' => $schema->number()->description('Depreciation period in months (required, 1-3600)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the depreciation was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new depreciation'),
|
||||
'name' => $schema->string()->description('Name of the new depreciation'),
|
||||
'months' => $schema->number()->description('Depreciation period in months'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_group')]
|
||||
#[Title('Create Group')]
|
||||
#[Description('Create a new Snipe-IT permission group. Requires superadmin. Permissions are a JSON object mapping permission keys to 1 (grant) or -1 (deny).')]
|
||||
class CreateGroupTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('superadmin')) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'permissions' => 'nullable|string',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$permissions = null;
|
||||
if ($request->filled('permissions')) {
|
||||
$result = $this->parseAndValidatePermissions($request->get('permissions'));
|
||||
if (is_string($result)) {
|
||||
return Response::make(Response::error($result));
|
||||
}
|
||||
$permissions = $result;
|
||||
}
|
||||
|
||||
$group = new Group;
|
||||
$group->name = $request->get('name');
|
||||
if ($permissions !== null) {
|
||||
$group->permissions = json_encode($permissions);
|
||||
}
|
||||
if ($request->filled('notes')) {
|
||||
$group->notes = $request->get('notes');
|
||||
}
|
||||
$group->created_by = auth()->id();
|
||||
|
||||
if ($group->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.group_created', ['name' => $group->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.group_created', ['name' => $group->name]),
|
||||
'id' => $group->id,
|
||||
'name' => $group->name,
|
||||
'permissions' => $group->decodePermissions(),
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $group->getErrors()->first()])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a JSON permissions string and validate all keys against config('permissions').
|
||||
* Returns the decoded array on success, or an error string on failure.
|
||||
*/
|
||||
private function parseAndValidatePermissions(string $raw): array|string
|
||||
{
|
||||
$decoded = json_decode($raw, true);
|
||||
if (! is_array($decoded)) {
|
||||
return trans('mcp.invalid_permissions_format');
|
||||
}
|
||||
|
||||
$validKeys = collect(config('permissions'))
|
||||
->flatMap(fn ($perms) => collect($perms)->pluck('permission'))
|
||||
->unique()
|
||||
->flip()
|
||||
->all();
|
||||
|
||||
foreach (array_keys($decoded) as $key) {
|
||||
if (! isset($validKeys[$key])) {
|
||||
return trans('mcp.invalid_permission_key', ['key' => $key]);
|
||||
}
|
||||
if (! in_array((int) $decoded[$key], [1, -1], true)) {
|
||||
return trans('mcp.invalid_permission_value', ['key' => $key]);
|
||||
}
|
||||
}
|
||||
|
||||
return array_map('intval', $decoded);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Group name (required, must be unique)'),
|
||||
'permissions' => $schema->string()->description(
|
||||
'JSON object mapping permission keys to 1 (grant) or -1 (deny). '.
|
||||
'Valid keys include: superuser, admin, import, reports.view, '.
|
||||
'assets.view, assets.create, assets.edit, assets.delete, assets.checkout, assets.checkin, assets.audit, '.
|
||||
'users.view, users.create, users.edit, users.delete, '.
|
||||
'licenses.view, licenses.create, licenses.edit, licenses.delete, licenses.checkout, licenses.checkin, '.
|
||||
'accessories.view, accessories.create, accessories.edit, accessories.delete, accessories.checkout, accessories.checkin, '.
|
||||
'components.view, components.create, components.edit, components.delete, components.checkout, components.checkin, '.
|
||||
'consumables.view, consumables.create, consumables.edit, consumables.delete, consumables.checkout, '.
|
||||
'and many more. Example: {"assets.view":1,"assets.create":1,"assets.edit":-1}'
|
||||
),
|
||||
'notes' => $schema->string()->description('Notes about the group'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the group was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new group'),
|
||||
'name' => $schema->string()->description('Name of the new group'),
|
||||
'permissions' => $schema->object()->description('Permissions set on the group'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\License;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_license')]
|
||||
#[Title('Create License')]
|
||||
#[Description('Create a new Snipe-IT software license')]
|
||||
class CreateLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', License::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'seats' => 'required|integer|min:1',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'purchase_date' => 'nullable|date_format:Y-m-d',
|
||||
'purchase_cost' => 'nullable|numeric|min:0',
|
||||
'purchase_order' => 'nullable|string|max:255',
|
||||
'order_number' => 'nullable|string|max:255',
|
||||
'expiration_date' => 'nullable|date_format:Y-m-d',
|
||||
'termination_date' => 'nullable|date_format:Y-m-d',
|
||||
'license_name' => 'nullable|string|max:255',
|
||||
'license_email' => 'nullable|email|max:255',
|
||||
'maintained' => 'nullable|boolean',
|
||||
'reassignable' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
'min_amt' => 'nullable|integer|min:0',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$license = new License;
|
||||
$license->fill($request->only([
|
||||
'name', 'seats', 'category_id', 'serial', 'manufacturer_id',
|
||||
'supplier_id', 'purchase_date', 'purchase_cost', 'purchase_order',
|
||||
'order_number', 'expiration_date', 'termination_date',
|
||||
'license_name', 'license_email', 'maintained', 'reassignable',
|
||||
'notes', 'min_amt',
|
||||
]));
|
||||
|
||||
$license->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$license->created_by = auth()->id();
|
||||
|
||||
if ($license->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_created', ['name' => $license->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_created', ['name' => $license->name]),
|
||||
'id' => $license->id,
|
||||
'name' => $license->name,
|
||||
'seats' => $license->seats,
|
||||
'category_id' => $license->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $license->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('License name (required)'),
|
||||
'seats' => $schema->number()->description('Number of seats (required, min 1)'),
|
||||
'category_id' => $schema->number()->description('Category ID — must be a license category (required)'),
|
||||
'serial' => $schema->string()->description('Product key / serial number'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'purchase_cost' => $schema->number()->description('Purchase cost'),
|
||||
'purchase_order' => $schema->string()->description('Purchase order number'),
|
||||
'order_number' => $schema->string()->description('Order number'),
|
||||
'expiration_date' => $schema->string()->description('License expiration date (YYYY-MM-DD)'),
|
||||
'termination_date' => $schema->string()->description('License termination date (YYYY-MM-DD)'),
|
||||
'license_name' => $schema->string()->description('Name of the licensed user/organization'),
|
||||
'license_email' => $schema->string()->description('Email of the licensed user/organization'),
|
||||
'maintained' => $schema->boolean()->description('Whether the license is under maintenance'),
|
||||
'reassignable' => $schema->boolean()->description('Whether seats can be reassigned after checkin'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'min_amt' => $schema->number()->description('Minimum seat threshold for alerts'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the license was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new license'),
|
||||
'name' => $schema->string()->description('Name of the new license'),
|
||||
'seats' => $schema->number()->description('Total seat count'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Location;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_location')]
|
||||
#[Title('Create Location')]
|
||||
#[Description('Create a new Snipe-IT location')]
|
||||
class CreateLocationTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Location::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'address' => 'nullable|string',
|
||||
'address2' => 'nullable|string',
|
||||
'city' => 'nullable|string',
|
||||
'state' => 'nullable|string',
|
||||
'country' => 'nullable|string',
|
||||
'zip' => 'nullable|string',
|
||||
'phone' => 'nullable|string|max:255',
|
||||
'fax' => 'nullable|string|max:255',
|
||||
'currency' => 'nullable|string',
|
||||
'parent_id' => 'nullable|integer|exists:locations,id',
|
||||
'manager_id' => 'nullable|integer|exists:users,id',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$location = new Location;
|
||||
$location->name = $request->get('name');
|
||||
|
||||
foreach (['address', 'address2', 'city', 'state', 'country', 'zip', 'phone', 'fax', 'currency', 'parent_id', 'manager_id'] as $field) {
|
||||
if ($request->filled($field)) {
|
||||
$location->{$field} = $request->get($field);
|
||||
}
|
||||
}
|
||||
|
||||
if ($location->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.location_created', ['name' => $location->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.location_created', ['name' => $location->name]),
|
||||
'id' => $location->id,
|
||||
'name' => $location->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $location->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Location name (required)'),
|
||||
'address' => $schema->string()->description('Street address'),
|
||||
'address2' => $schema->string()->description('Address line 2'),
|
||||
'city' => $schema->string()->description('City'),
|
||||
'state' => $schema->string()->description('State'),
|
||||
'country' => $schema->string()->description('Country'),
|
||||
'zip' => $schema->string()->description('Zip code'),
|
||||
'phone' => $schema->string()->description('Phone number'),
|
||||
'fax' => $schema->string()->description('Fax number'),
|
||||
'currency' => $schema->string()->description('Currency code'),
|
||||
'parent_id' => $schema->number()->description('Parent location ID'),
|
||||
'manager_id' => $schema->number()->description('Manager user ID'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the location was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new location'),
|
||||
'name' => $schema->string()->description('Name of the new location'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Maintenance;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_maintenance')]
|
||||
#[Title('Create Maintenance')]
|
||||
#[Description('Create a new asset maintenance record')]
|
||||
class CreateMaintenanceTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('update', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'asset_id' => 'required|integer|exists:assets,id',
|
||||
'title' => 'required|string|max:255',
|
||||
'asset_maintenance_type' => 'nullable|string|max:255',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'is_warranty' => 'nullable|boolean',
|
||||
'cost' => 'nullable|numeric|min:0',
|
||||
'start_date' => 'nullable|date_format:Y-m-d',
|
||||
'completion_date' => 'nullable|date_format:Y-m-d',
|
||||
'notes' => 'nullable|string',
|
||||
'user_id' => 'nullable|integer|exists:users,id',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$maintenance = new Maintenance;
|
||||
$maintenance->asset_id = $request->get('asset_id');
|
||||
$maintenance->name = $request->get('title');
|
||||
$maintenance->asset_maintenance_type = $request->get('asset_maintenance_type', 'Maintenance');
|
||||
$maintenance->start_date = $request->filled('start_date') ? $request->get('start_date') : now()->format('Y-m-d');
|
||||
$maintenance->created_by = auth()->id();
|
||||
$maintenance->is_warranty = 0;
|
||||
|
||||
foreach (['supplier_id', 'is_warranty', 'cost', 'completion_date', 'notes', 'user_id'] as $field) {
|
||||
if ($request->filled($field)) {
|
||||
$maintenance->{$field} = $request->get($field);
|
||||
}
|
||||
}
|
||||
|
||||
if ($maintenance->save()) {
|
||||
$maintenance->load('asset');
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.maintenance_created', ['name' => $maintenance->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.maintenance_created', ['name' => $maintenance->name]),
|
||||
'id' => $maintenance->id,
|
||||
'title' => $maintenance->name,
|
||||
'asset_id' => $maintenance->asset_id,
|
||||
'asset_tag' => $maintenance->asset?->asset_tag,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $maintenance->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_id' => $schema->number()->description('Asset ID the maintenance is for (required)'),
|
||||
'title' => $schema->string()->description('Maintenance title/name (required)'),
|
||||
'asset_maintenance_type' => $schema->string()->description('Type of maintenance (e.g. maintenance, repair, upgrade)'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'is_warranty' => $schema->boolean()->description('Whether this is a warranty maintenance'),
|
||||
'cost' => $schema->number()->description('Cost of the maintenance'),
|
||||
'start_date' => $schema->string()->description('Start date (YYYY-MM-DD, defaults to today)'),
|
||||
'completion_date' => $schema->string()->description('Completion date (YYYY-MM-DD)'),
|
||||
'notes' => $schema->string()->description('Notes about the maintenance'),
|
||||
'user_id' => $schema->number()->description('Technician user ID'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the maintenance was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new maintenance record'),
|
||||
'title' => $schema->string()->description('Title of the maintenance'),
|
||||
'asset_id' => $schema->number()->description('Asset ID'),
|
||||
'asset_tag' => $schema->string()->description('Asset tag'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Manufacturer;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_manufacturer')]
|
||||
#[Title('Create Manufacturer')]
|
||||
#[Description('Create a new Snipe-IT manufacturer')]
|
||||
class CreateManufacturerTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Manufacturer::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'url' => 'nullable|string|max:255',
|
||||
'support_url' => 'nullable|string|max:255',
|
||||
'support_email' => 'nullable|email|max:191',
|
||||
'support_phone' => 'nullable|string|max:191',
|
||||
'warranty_lookup_url' => 'nullable|string|max:255',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$manufacturer = new Manufacturer;
|
||||
$manufacturer->fill($request->only([
|
||||
'name', 'url', 'support_url', 'support_email', 'support_phone', 'warranty_lookup_url', 'notes',
|
||||
]));
|
||||
|
||||
if ($manufacturer->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.manufacturer_created', ['name' => $manufacturer->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.manufacturer_created', ['name' => $manufacturer->name]),
|
||||
'id' => $manufacturer->id,
|
||||
'name' => $manufacturer->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $manufacturer->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Manufacturer name (required)'),
|
||||
'url' => $schema->string()->description('Manufacturer website URL'),
|
||||
'support_url' => $schema->string()->description('Support website URL'),
|
||||
'support_email' => $schema->string()->description('Support email address'),
|
||||
'support_phone' => $schema->string()->description('Support phone number'),
|
||||
'warranty_lookup_url' => $schema->string()->description('Warranty lookup URL'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the manufacturer was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new manufacturer'),
|
||||
'name' => $schema->string()->description('Name of the new manufacturer'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Statuslabel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_status_label')]
|
||||
#[Title('Create Status Label')]
|
||||
#[Description('Create a new Snipe-IT status label')]
|
||||
class CreateStatusLabelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Statuslabel::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|string|in:deployable,pending,archived,undeployable',
|
||||
'color' => 'nullable|string',
|
||||
'notes' => 'nullable|string',
|
||||
'default_label' => 'nullable|boolean',
|
||||
'show_in_nav' => 'nullable|boolean',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$statuslabel = new Statuslabel;
|
||||
$statuslabel->name = $request->get('name');
|
||||
|
||||
$statusType = Statuslabel::getStatuslabelTypesForDB($request->get('type'));
|
||||
$statuslabel->deployable = $statusType['deployable'];
|
||||
$statuslabel->pending = $statusType['pending'];
|
||||
$statuslabel->archived = $statusType['archived'];
|
||||
|
||||
if ($request->filled('color')) {
|
||||
$statuslabel->color = $request->get('color');
|
||||
}
|
||||
if ($request->filled('notes')) {
|
||||
$statuslabel->notes = $request->get('notes');
|
||||
}
|
||||
$statuslabel->default_label = $request->get('default_label', 0);
|
||||
$statuslabel->show_in_nav = $request->get('show_in_nav', 0);
|
||||
|
||||
if ($statuslabel->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.status_label_created', ['name' => $statuslabel->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.status_label_created', ['name' => $statuslabel->name]),
|
||||
'id' => $statuslabel->id,
|
||||
'name' => $statuslabel->name,
|
||||
'type' => $statuslabel->getStatuslabelType(),
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $statuslabel->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Status label name (required)'),
|
||||
'type' => $schema->string()->description('Status label type: deployable, pending, archived, or undeployable (required)'),
|
||||
'color' => $schema->string()->description('Display color in #RRGGBB format'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'default_label' => $schema->boolean()->description('Whether this is the default label'),
|
||||
'show_in_nav' => $schema->boolean()->description('Whether to show in navigation'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the status label was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new status label'),
|
||||
'name' => $schema->string()->description('Name of the new status label'),
|
||||
'type' => $schema->string()->description('Type of the new status label'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Supplier;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_supplier')]
|
||||
#[Title('Create Supplier')]
|
||||
#[Description('Create a new Snipe-IT supplier')]
|
||||
class CreateSupplierTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Supplier::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'address' => 'nullable|string',
|
||||
'address2' => 'nullable|string',
|
||||
'city' => 'nullable|string',
|
||||
'state' => 'nullable|string',
|
||||
'country' => 'nullable|string',
|
||||
'zip' => 'nullable|string',
|
||||
'phone' => 'nullable|string',
|
||||
'fax' => 'nullable|string',
|
||||
'email' => 'nullable|email',
|
||||
'url' => 'nullable|string',
|
||||
'contact' => 'nullable|string',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$supplier = new Supplier;
|
||||
$supplier->fill($request->only([
|
||||
'name', 'address', 'address2', 'city', 'state', 'country', 'zip',
|
||||
'phone', 'fax', 'email', 'url', 'contact', 'notes',
|
||||
]));
|
||||
|
||||
if ($supplier->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.supplier_created', ['name' => $supplier->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.supplier_created', ['name' => $supplier->name]),
|
||||
'id' => $supplier->id,
|
||||
'name' => $supplier->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $supplier->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Supplier name (required)'),
|
||||
'address' => $schema->string()->description('Address line 1'),
|
||||
'address2' => $schema->string()->description('Address line 2'),
|
||||
'city' => $schema->string()->description('City'),
|
||||
'state' => $schema->string()->description('State'),
|
||||
'country' => $schema->string()->description('Country'),
|
||||
'zip' => $schema->string()->description('Postal code'),
|
||||
'phone' => $schema->string()->description('Phone number'),
|
||||
'fax' => $schema->string()->description('Fax number'),
|
||||
'email' => $schema->string()->description('Email address'),
|
||||
'url' => $schema->string()->description('Website URL'),
|
||||
'contact' => $schema->string()->description('Contact name'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the supplier was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new supplier'),
|
||||
'name' => $schema->string()->description('Name of the new supplier'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Group;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_user')]
|
||||
#[Title('Create User')]
|
||||
#[Description('Create a new Snipe-IT user account')]
|
||||
class CreateUserTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', User::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'first_name' => 'required|string|max:191',
|
||||
'last_name' => 'nullable|string|max:191',
|
||||
'username' => 'required|string|max:191',
|
||||
'email' => 'nullable|email|max:191',
|
||||
'password' => 'nullable|string|min:8',
|
||||
'employee_num' => 'nullable|string|max:191',
|
||||
'jobtitle' => 'nullable|string|max:191',
|
||||
'phone' => 'nullable|string|max:35',
|
||||
'mobile' => 'nullable|string|max:35',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'department_id' => 'nullable|integer|exists:departments,id',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'manager_id' => 'nullable|integer|exists:users,id',
|
||||
'activated' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
'start_date' => 'nullable|date_format:Y-m-d',
|
||||
'end_date' => 'nullable|date_format:Y-m-d',
|
||||
'vip' => 'nullable|boolean',
|
||||
'remote' => 'nullable|boolean',
|
||||
'website' => 'nullable|url|max:191',
|
||||
'address' => 'nullable|string|max:191',
|
||||
'city' => 'nullable|string|max:191',
|
||||
'state' => 'nullable|string|max:191',
|
||||
'country' => 'nullable|string|max:191',
|
||||
'zip' => 'nullable|string|max:10',
|
||||
'group_ids' => 'nullable|array',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
if (User::where('username', $request->get('username'))->exists()) {
|
||||
return Response::make(Response::error(trans('mcp.username_taken', ['username' => $request->get('username')])));
|
||||
}
|
||||
|
||||
$user = new User;
|
||||
$user->fill($request->only([
|
||||
'first_name', 'last_name', 'username', 'email', 'employee_num',
|
||||
'jobtitle', 'phone', 'mobile', 'department_id', 'location_id',
|
||||
'manager_id', 'notes', 'start_date', 'end_date', 'vip', 'remote',
|
||||
'website', 'address', 'city', 'state', 'country', 'zip',
|
||||
]));
|
||||
|
||||
$user->activated = $request->filled('activated') ? (bool) $request->get('activated') : true;
|
||||
$user->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$user->created_by = auth()->id();
|
||||
|
||||
if ($request->filled('password')) {
|
||||
$user->password = bcrypt($request->get('password'));
|
||||
} else {
|
||||
$user->password = $user->noPassword();
|
||||
}
|
||||
|
||||
if ($user->save()) {
|
||||
$groupIds = [];
|
||||
if ($request->filled('group_ids') && auth()->user()->isSuperUser()) {
|
||||
$groupIds = Group::whereIn('id', $request->get('group_ids'))->pluck('id')->all();
|
||||
$user->groups()->sync($groupIds);
|
||||
} elseif ($request->filled('group_ids')) {
|
||||
return Response::make(Response::error(trans('mcp.superadmin_required_for_groups')));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.user_created', ['username' => $user->username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.user_created', ['username' => $user->username]),
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'email' => $user->email,
|
||||
'first_name' => $user->first_name,
|
||||
'last_name' => $user->last_name,
|
||||
'group_ids' => $groupIds,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $user->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'first_name' => $schema->string()->description('First name (required)'),
|
||||
'last_name' => $schema->string()->description('Last name'),
|
||||
'username' => $schema->string()->description('Username (required, must be unique)'),
|
||||
'email' => $schema->string()->description('Email address'),
|
||||
'password' => $schema->string()->description('Password (min 8 characters; if omitted, account will have no password set)'),
|
||||
'employee_num' => $schema->string()->description('Employee number'),
|
||||
'jobtitle' => $schema->string()->description('Job title'),
|
||||
'phone' => $schema->string()->description('Phone number'),
|
||||
'mobile' => $schema->string()->description('Mobile number'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'department_id' => $schema->number()->description('Department ID'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'manager_id' => $schema->number()->description('Manager user ID'),
|
||||
'activated' => $schema->boolean()->description('Whether the account is active (default: true)'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'start_date' => $schema->string()->description('Employment start date (YYYY-MM-DD)'),
|
||||
'end_date' => $schema->string()->description('Employment end date (YYYY-MM-DD)'),
|
||||
'vip' => $schema->boolean()->description('Mark user as VIP'),
|
||||
'remote' => $schema->boolean()->description('Mark user as remote'),
|
||||
'website' => $schema->string()->description('Website URL'),
|
||||
'address' => $schema->string()->description('Street address'),
|
||||
'city' => $schema->string()->description('City'),
|
||||
'state' => $schema->string()->description('State/province'),
|
||||
'country' => $schema->string()->description('Country'),
|
||||
'zip' => $schema->string()->description('Postal/ZIP code'),
|
||||
'group_ids' => $schema->array()->description('Array of permission group IDs to assign (requires superadmin). Example: [1, 3]'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the user was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new user'),
|
||||
'username' => $schema->string()->description('Username of the new user'),
|
||||
'email' => $schema->string()->description('Email of the new user'),
|
||||
'first_name' => $schema->string()->description('First name'),
|
||||
'last_name' => $schema->string()->description('Last name'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_accessory')]
|
||||
#[Title('Delete Accessory')]
|
||||
#[Description('Soft-delete a Snipe-IT accessory. The accessory must have no units currently checked out.')]
|
||||
class DeleteAccessoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$accessory = $this->resolveAccessory($request);
|
||||
|
||||
if (! $accessory) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $accessory)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($accessory->numCheckedOut() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_has_checkouts')));
|
||||
}
|
||||
|
||||
$name = $accessory->name;
|
||||
|
||||
$accessory->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.accessory_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.accessory_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAccessory(Request $request): ?Accessory
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Accessory::withCount('checkouts as checkouts_count')->find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Accessory::withCount('checkouts as checkouts_count')->where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the accessory to delete'),
|
||||
'name' => $schema->string()->description('Name of the accessory to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted accessory'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\AssetModel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_asset_model')]
|
||||
#[Title('Delete Asset Model')]
|
||||
#[Description('Soft-delete a Snipe-IT asset model by numeric ID or name')]
|
||||
class DeleteAssetModelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$model = $this->resolveModel($request);
|
||||
|
||||
if (! $model) {
|
||||
return Response::make(Response::error(trans('mcp.asset_model_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $model)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($model->assets()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.model_has_assets')));
|
||||
}
|
||||
|
||||
$name = $model->name;
|
||||
|
||||
$model->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_model_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_model_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveModel(Request $request): ?AssetModel
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return AssetModel::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return AssetModel::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the asset model to delete'),
|
||||
'name' => $schema->string()->description('Name of the asset model to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted asset model'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_asset')]
|
||||
#[Title('Delete Asset')]
|
||||
#[Description('Soft-delete a Snipe-IT asset. If the asset is currently checked out it will be checked in first.')]
|
||||
class DeleteAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'id' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$assetTag = $asset->asset_tag;
|
||||
|
||||
if ($asset->assignedTo) {
|
||||
$target = $asset->assignedTo;
|
||||
$originalValues = $asset->getRawOriginal();
|
||||
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checked in on delete', date('Y-m-d H:i:s'), $originalValues));
|
||||
DB::table('assets')->where('id', $asset->id)->update(['assigned_to' => null]);
|
||||
}
|
||||
|
||||
$asset->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_deleted', ['asset_tag' => $assetTag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_deleted', ['asset_tag' => $assetTag]),
|
||||
'asset_tag' => $assetTag,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))->first();
|
||||
}
|
||||
if ($request->filled('serial')) {
|
||||
return Asset::where('serial', $request->get('serial'))->first();
|
||||
}
|
||||
if ($request->filled('id')) {
|
||||
return Asset::find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset to delete'),
|
||||
'serial' => $schema->string()->description('Serial number of the asset to delete'),
|
||||
'id' => $schema->number()->description('Numeric ID of the asset to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'error' => $schema->boolean()->description('True if the deletion failed'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the deleted asset'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_category')]
|
||||
#[Title('Delete Category')]
|
||||
#[Description('Soft-delete a Snipe-IT category. The category must have no items assigned to it.')]
|
||||
class DeleteCategoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$category = $this->resolveCategory($request);
|
||||
|
||||
if (! $category) {
|
||||
return Response::make(Response::error(trans('mcp.category_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $category)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $category->name;
|
||||
|
||||
try {
|
||||
$category->delete();
|
||||
} catch (\Exception $e) {
|
||||
return Response::make(Response::error(trans('mcp.category_delete_failed', ['error' => $e->getMessage()])));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.category_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.category_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveCategory(Request $request): ?Category
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Category::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Category::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the category to delete'),
|
||||
'name' => $schema->string()->description('Name of the category to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted category'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_company')]
|
||||
#[Title('Delete Company')]
|
||||
#[Description('Soft-delete a Snipe-IT company by numeric ID or name')]
|
||||
class DeleteCompanyTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$company = $this->resolveCompany($request);
|
||||
|
||||
if (! $company) {
|
||||
return Response::make(Response::error(trans('mcp.company_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $company)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $company->name;
|
||||
|
||||
$company->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.company_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.company_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveCompany(Request $request): ?Company
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Company::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Company::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the company to delete'),
|
||||
'name' => $schema->string()->description('Name of the company to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted company'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Component;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_component')]
|
||||
#[Title('Delete Component')]
|
||||
#[Description('Soft-delete a Snipe-IT component. The component must have no units currently checked out to assets.')]
|
||||
class DeleteComponentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:191',
|
||||
]);
|
||||
|
||||
$component = $this->resolveComponent($request);
|
||||
|
||||
if (! $component) {
|
||||
return Response::make(Response::error(trans('mcp.component_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $component)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($component->numCheckedOut() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.component_has_checkouts')));
|
||||
}
|
||||
|
||||
$name = $component->name;
|
||||
|
||||
$component->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.component_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.component_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveComponent(Request $request): ?Component
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Component::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Component::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the component to delete'),
|
||||
'name' => $schema->string()->description('Name of the component to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted component'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Consumable;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_consumable')]
|
||||
#[Title('Delete Consumable')]
|
||||
#[Description('Soft-delete a Snipe-IT consumable. The consumable must have no units currently checked out.')]
|
||||
class DeleteConsumableTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$consumable = $this->resolveConsumable($request);
|
||||
|
||||
if (! $consumable) {
|
||||
return Response::make(Response::error(trans('mcp.consumable_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $consumable)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($consumable->users()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.consumable_has_checkouts')));
|
||||
}
|
||||
|
||||
$name = $consumable->name;
|
||||
|
||||
$consumable->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.consumable_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.consumable_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveConsumable(Request $request): ?Consumable
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Consumable::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Consumable::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the consumable to delete'),
|
||||
'name' => $schema->string()->description('Name of the consumable to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted consumable'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Department;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_department')]
|
||||
#[Title('Delete Department')]
|
||||
#[Description('Soft-delete a Snipe-IT department. The department must have no users assigned to it.')]
|
||||
class DeleteDepartmentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$department = $this->resolveDepartment($request);
|
||||
|
||||
if (! $department) {
|
||||
return Response::make(Response::error(trans('mcp.department_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $department)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($department->users->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.department_has_users')));
|
||||
}
|
||||
|
||||
$name = $department->name;
|
||||
|
||||
$department->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.department_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.department_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveDepartment(Request $request): ?Department
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Department::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Department::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the department to delete'),
|
||||
'name' => $schema->string()->description('Name of the department to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted department'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Depreciation;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_depreciation')]
|
||||
#[Title('Delete Depreciation')]
|
||||
#[Description('Soft-delete a Snipe-IT depreciation schedule by numeric ID or name')]
|
||||
class DeleteDepreciationTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$dep = $this->resolveDepreciation($request);
|
||||
|
||||
if (! $dep) {
|
||||
return Response::make(Response::error(trans('mcp.depreciation_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $dep)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $dep->name;
|
||||
|
||||
$dep->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.depreciation_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.depreciation_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveDepreciation(Request $request): ?Depreciation
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Depreciation::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Depreciation::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the depreciation to delete'),
|
||||
'name' => $schema->string()->description('Name of the depreciation to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted depreciation'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_group')]
|
||||
#[Title('Delete Group')]
|
||||
#[Description('Delete a Snipe-IT permission group by ID or name. The group must have no users assigned.')]
|
||||
class DeleteGroupTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('superadmin')) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
if ($request->filled('id')) {
|
||||
$group = Group::find($request->get('id'));
|
||||
} elseif ($request->filled('name')) {
|
||||
$group = Group::where('name', $request->get('name'))->first();
|
||||
} else {
|
||||
return Response::make(Response::error(trans('mcp.id_or_name_required')));
|
||||
}
|
||||
|
||||
if (! $group) {
|
||||
return Response::make(Response::error(trans('mcp.group_not_found')));
|
||||
}
|
||||
|
||||
$groupName = $group->name;
|
||||
|
||||
if ($group->delete()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.group_deleted', ['name' => $groupName]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.group_deleted', ['name' => $groupName]),
|
||||
'name' => $groupName,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.delete_failed')));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric group ID to delete'),
|
||||
'name' => $schema->string()->description('Group name to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted group'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\License;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_license')]
|
||||
#[Title('Delete License')]
|
||||
#[Description('Soft-delete a Snipe-IT license. The license must have no seats currently assigned to users or assets.')]
|
||||
class DeleteLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$license = $this->resolveLicense($request);
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error(trans('mcp.license_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $license)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($license->assignedCount()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.license_has_seats_assigned')));
|
||||
}
|
||||
|
||||
$name = $license->name;
|
||||
|
||||
DB::table('license_seats')
|
||||
->where('license_id', $license->id)
|
||||
->update(['assigned_to' => null, 'asset_id' => null]);
|
||||
|
||||
$license->licenseseats()->delete();
|
||||
$license->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveLicense(Request $request): ?License
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return License::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return License::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the license to delete'),
|
||||
'name' => $schema->string()->description('Name of the license to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted license'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Location;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_location')]
|
||||
#[Title('Delete Location')]
|
||||
#[Description('Soft-delete a Snipe-IT location by numeric ID or name')]
|
||||
class DeleteLocationTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$location = $this->resolveLocation($request);
|
||||
|
||||
if (! $location) {
|
||||
return Response::make(Response::error(trans('mcp.location_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $location)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($location->users()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.location_has_users')));
|
||||
}
|
||||
|
||||
if ($location->children()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.location_has_child_locations')));
|
||||
}
|
||||
|
||||
$name = $location->name;
|
||||
|
||||
$location->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.location_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.location_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveLocation(Request $request): ?Location
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Location::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Location::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the location to delete'),
|
||||
'name' => $schema->string()->description('Name of the location to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted location'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Manufacturer;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_manufacturer')]
|
||||
#[Title('Delete Manufacturer')]
|
||||
#[Description('Soft-delete a Snipe-IT manufacturer identified by numeric ID or name')]
|
||||
class DeleteManufacturerTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$manufacturer = $this->resolveManufacturer($request);
|
||||
|
||||
if (! $manufacturer) {
|
||||
return Response::make(Response::error(trans('mcp.manufacturer_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $manufacturer)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $manufacturer->name;
|
||||
|
||||
$manufacturer->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.manufacturer_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.manufacturer_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveManufacturer(Request $request): ?Manufacturer
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Manufacturer::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Manufacturer::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the manufacturer to delete'),
|
||||
'name' => $schema->string()->description('Name of the manufacturer to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted manufacturer'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Statuslabel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_status_label')]
|
||||
#[Title('Delete Status Label')]
|
||||
#[Description('Soft-delete a Snipe-IT status label identified by numeric ID or name')]
|
||||
class DeleteStatusLabelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$label = $this->resolveStatusLabel($request);
|
||||
|
||||
if (! $label) {
|
||||
return Response::make(Response::error(trans('mcp.status_label_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $label)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($label->assets()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.status_label_has_assets')));
|
||||
}
|
||||
|
||||
$name = $label->name;
|
||||
|
||||
$label->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.status_label_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.status_label_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveStatusLabel(Request $request): ?Statuslabel
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Statuslabel::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Statuslabel::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the status label to delete'),
|
||||
'name' => $schema->string()->description('Name of the status label to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted status label'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Supplier;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_supplier')]
|
||||
#[Title('Delete Supplier')]
|
||||
#[Description('Soft-delete a Snipe-IT supplier identified by numeric ID or name')]
|
||||
class DeleteSupplierTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$supplier = $this->resolveSupplier($request);
|
||||
|
||||
if (! $supplier) {
|
||||
return Response::make(Response::error(trans('mcp.supplier_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $supplier)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $supplier->name;
|
||||
|
||||
$supplier->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.supplier_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.supplier_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveSupplier(Request $request): ?Supplier
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Supplier::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Supplier::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the supplier to delete'),
|
||||
'name' => $schema->string()->description('Name of the supplier to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted supplier'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_user')]
|
||||
#[Title('Delete User')]
|
||||
#[Description('Soft-delete a Snipe-IT user. The user must have no assets, licenses, accessories, or consumables assigned.')]
|
||||
class DeleteUserTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'username' => 'nullable|string|max:191',
|
||||
'email' => 'nullable|string|max:191',
|
||||
]);
|
||||
|
||||
$user = $this->resolveUser($request);
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $user)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($user->id === auth()->id()) {
|
||||
return Response::make(Response::error(trans('mcp.user_cannot_delete_self')));
|
||||
}
|
||||
|
||||
if ($user->allAssignedCount() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.user_has_items')));
|
||||
}
|
||||
|
||||
$username = $user->username;
|
||||
|
||||
if ($user->delete()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.user_deleted', ['username' => $username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.user_deleted', ['username' => $username]),
|
||||
'username' => $username,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.delete_failed_error', ['error' => $user->getErrors()->first()])));
|
||||
}
|
||||
|
||||
private function resolveUser(Request $request): ?User
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return User::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('username')) {
|
||||
return User::where('username', $request->get('username'))->first();
|
||||
}
|
||||
if ($request->filled('email')) {
|
||||
return User::where('email', $request->get('email'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric user ID to delete'),
|
||||
'username' => $schema->string()->description('Username of the user to delete'),
|
||||
'email' => $schema->string()->description('Email address of the user to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'username' => $schema->string()->description('Username of the deleted user'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Actionlog;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('get_activity_log')]
|
||||
#[Title('Get Activity Log')]
|
||||
#[Description('Retrieve the Snipe-IT activity log with optional filtering by item type, item ID, user, and action type')]
|
||||
class GetActivityLogTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('activity.view')) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'item_type' => 'nullable|string|max:255',
|
||||
'item_id' => 'nullable|integer',
|
||||
'user_id' => 'nullable|integer',
|
||||
'action_type' => 'nullable|string|max:255',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$logs = Actionlog::with('user', 'item')->orderBy('created_at', 'desc');
|
||||
|
||||
if ($request->filled('item_type')) {
|
||||
$logs->where('item_type', $request->get('item_type'));
|
||||
}
|
||||
|
||||
if ($request->filled('item_id')) {
|
||||
$logs->where('item_id', $request->get('item_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('user_id')) {
|
||||
$logs->where('user_id', $request->get('user_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('action_type')) {
|
||||
$logs->where('action_type', $request->get('action_type'));
|
||||
}
|
||||
|
||||
$total = $logs->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $logs->skip($offset)->take($limit)->get();
|
||||
|
||||
$activityData = $results->map(fn (Actionlog $log) => [
|
||||
'id' => $log->id,
|
||||
'action_type' => $log->action_type,
|
||||
'item_type' => $log->item_type,
|
||||
'item_id' => $log->item_id,
|
||||
'user_id' => $log->user_id,
|
||||
'user' => $log->user?->username,
|
||||
'note' => $log->note,
|
||||
'created_at' => $log->created_at?->toDateTimeString(),
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_activity', ['total' => $total, 'count' => count($activityData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'activity' => $activityData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'item_type' => $schema->string()->description('Filter by item type (e.g. App\\Models\\Asset)'),
|
||||
'item_id' => $schema->number()->description('Filter by item ID'),
|
||||
'user_id' => $schema->number()->description('Filter by user ID'),
|
||||
'action_type' => $schema->string()->description('Filter by action type (e.g. checkout, checkin, update)'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching log entries')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'activity' => $schema->array()->description('List of activity log entries')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('get_current_user')]
|
||||
#[Title('Get Current User')]
|
||||
#[Description('Return information about the currently authenticated Snipe-IT user')]
|
||||
class GetCurrentUserTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! auth()->check()) {
|
||||
return Response::make(Response::error(trans('mcp.not_authenticated')));
|
||||
}
|
||||
|
||||
$user = User::with('company', 'department', 'userloc')->find(auth()->id());
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.not_authenticated')));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.current_user', ['username' => $user->username]))
|
||||
)->withStructuredContent([
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'first_name' => $user->first_name,
|
||||
'last_name' => $user->last_name,
|
||||
'email' => $user->email,
|
||||
'company' => $user->company?->name,
|
||||
'department' => $user->department?->name,
|
||||
'location' => $user->userloc?->name,
|
||||
'employee_num' => $user->employee_num,
|
||||
'title' => $user->jobtitle,
|
||||
'phone' => $user->phone,
|
||||
'activated' => (bool) $user->activated,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric user ID')->required(),
|
||||
'username' => $schema->string()->description('Username')->required(),
|
||||
'first_name' => $schema->string()->description('First name'),
|
||||
'last_name' => $schema->string()->description('Last name'),
|
||||
'email' => $schema->string()->description('Email address'),
|
||||
'company' => $schema->string()->description('Company name'),
|
||||
'department' => $schema->string()->description('Department name'),
|
||||
'location' => $schema->string()->description('Default location name'),
|
||||
'employee_num' => $schema->string()->description('Employee number'),
|
||||
'title' => $schema->string()->description('Job title'),
|
||||
'phone' => $schema->string()->description('Phone number'),
|
||||
'activated' => $schema->boolean()->description('Whether the account is activated'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('get_user_assets')]
|
||||
#[Title('Get User Assets')]
|
||||
#[Description('Return all assets currently checked out to a Snipe-IT user')]
|
||||
class GetUserAssetsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', User::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$user = User::find($request->get('id'));
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
|
||||
$assets = Asset::where('assigned_to', $user->id)
|
||||
->where('assigned_type', User::class)
|
||||
->with('model', 'status', 'location')
|
||||
->get();
|
||||
|
||||
$data = $assets->map(fn ($asset) => [
|
||||
'id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'name' => $asset->name,
|
||||
'serial' => $asset->serial,
|
||||
'model' => $asset->model?->name,
|
||||
'status' => $asset->status?->name,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.user_assets_found', ['count' => count($data), 'username' => $user->username]))
|
||||
)->withStructuredContent([
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'total' => count($data),
|
||||
'assets' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the user whose assets should be listed (required)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'user_id' => $schema->number()->description('Numeric ID of the user')->required(),
|
||||
'username' => $schema->string()->description('Username of the user')->required(),
|
||||
'total' => $schema->number()->description('Total number of assets checked out to the user')->required(),
|
||||
'assets' => $schema->array()->description('List of checked-out assets'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\AssetModel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_asset_models')]
|
||||
#[Title('List Asset Models')]
|
||||
#[Description('Search and list Snipe-IT asset models with optional filtering and pagination')]
|
||||
class ListAssetModelsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', AssetModel::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'category_id' => 'nullable|integer',
|
||||
'manufacturer_id' => 'nullable|integer',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$models = AssetModel::with('category', 'manufacturer', 'depreciation')
|
||||
->withCount('assets as assets_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$models->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$models->where('category_id', $request->get('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('manufacturer_id')) {
|
||||
$models->where('manufacturer_id', $request->get('manufacturer_id'));
|
||||
}
|
||||
|
||||
$models->orderBy('models.created_at', 'desc');
|
||||
|
||||
$total = $models->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $models->skip($offset)->take($limit)->get();
|
||||
|
||||
$modelsData = $results->map(fn (AssetModel $model) => [
|
||||
'id' => $model->id,
|
||||
'name' => $model->name,
|
||||
'model_number' => $model->model_number,
|
||||
'category_id' => $model->category_id,
|
||||
'category' => $model->category?->name,
|
||||
'manufacturer_id' => $model->manufacturer_id,
|
||||
'manufacturer' => $model->manufacturer?->name,
|
||||
'assets_count' => $model->assets_count,
|
||||
'eol' => $model->eol,
|
||||
'min_amt' => $model->min_amt,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_asset_models', ['total' => $total, 'count' => count($modelsData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'models' => $modelsData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across model name, model number'),
|
||||
'category_id' => $schema->number()->description('Filter by category ID'),
|
||||
'manufacturer_id' => $schema->number()->description('Filter by manufacturer ID'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching asset models')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'models' => $schema->array()->description('List of asset models')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_asset_notes')]
|
||||
#[Title('List Asset Notes')]
|
||||
#[Description('List manual notes added to a Snipe-IT asset, identified by asset tag, serial number, or numeric ID')]
|
||||
class ListAssetNotesTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|string|max:100',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'id' => 'nullable|integer',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$query = Actionlog::with('adminuser:id,username')
|
||||
->where('item_type', Asset::class)
|
||||
->where('item_id', $asset->id)
|
||||
->where('action_type', 'note added')
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$records = $query->skip($offset)->take($limit)
|
||||
->get(['id', 'created_at', 'note', 'created_by', 'item_id', 'action_type']);
|
||||
|
||||
$notes = $records->map(fn ($n) => [
|
||||
'id' => $n->id,
|
||||
'created_at' => $n->created_at?->toISOString(),
|
||||
'note' => $n->note,
|
||||
'created_by_id' => $n->created_by,
|
||||
'created_by_username' => $n->adminuser?->username,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_asset_notes', [
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'total' => $total,
|
||||
'count' => count($notes),
|
||||
]))
|
||||
)->withStructuredContent([
|
||||
'asset_id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'notes' => $notes,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))->first();
|
||||
}
|
||||
if ($request->filled('serial')) {
|
||||
return Asset::where('serial', $request->get('serial'))->first();
|
||||
}
|
||||
if ($request->filled('id')) {
|
||||
return Asset::find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset'),
|
||||
'serial' => $schema->string()->description('Serial number of the asset'),
|
||||
'id' => $schema->number()->description('Numeric ID of the asset'),
|
||||
'limit' => $schema->number()->description('Number of notes to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of notes to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_id' => $schema->number()->description('Numeric ID of the asset')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset')->required(),
|
||||
'total' => $schema->number()->description('Total number of notes on this asset')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'notes' => $schema->array()->description('List of notes'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_assets')]
|
||||
#[Title('List Assets')]
|
||||
#[Description('Search and list Snipe-IT assets with optional filtering by keyword, status, and pagination')]
|
||||
class ListAssetsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('index', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'status_type' => 'nullable|string|in:RTD,Deployed,Archived,Pending,Undeployable',
|
||||
'company_id' => 'nullable|integer',
|
||||
'location_id' => 'nullable|integer',
|
||||
'category_id' => 'nullable|integer',
|
||||
'model_id' => 'nullable|integer',
|
||||
'manufacturer_id' => 'nullable|integer',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$assets = Asset::select('assets.*')
|
||||
->with('status', 'assignedTo', 'model.category', 'model.manufacturer', 'location', 'company');
|
||||
|
||||
match ($request->filled('status_type') ? $request->get('status_type') : null) {
|
||||
'RTD' => $assets->rtd(),
|
||||
'Deployed' => $assets->deployed(),
|
||||
'Archived' => $assets->archived(),
|
||||
'Pending' => $assets->pending(),
|
||||
'Undeployable' => $assets->undeployable(),
|
||||
default => $assets->notArchived(),
|
||||
};
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$assets->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$assets->where('assets.company_id', '=', $request->get('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$assets->where('assets.location_id', '=', $request->get('location_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$assets->inCategory($request->get('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('model_id')) {
|
||||
$assets->inModels([$request->get('model_id')]);
|
||||
}
|
||||
|
||||
if ($request->filled('manufacturer_id')) {
|
||||
$assets->byManufacturer($request->get('manufacturer_id'));
|
||||
}
|
||||
|
||||
$assets->orderBy('assets.created_at', 'desc');
|
||||
|
||||
$total = $assets->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $assets->skip($offset)->take($limit)->get();
|
||||
|
||||
$assetsData = $results->map(fn (Asset $asset) => [
|
||||
'id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'name' => $asset->name,
|
||||
'serial' => $asset->serial,
|
||||
'status' => $asset->status?->name,
|
||||
'status_type' => $asset->status?->getStatuslabelType(),
|
||||
'model' => $asset->model?->name,
|
||||
'category' => $asset->model?->category?->name,
|
||||
'manufacturer' => $asset->model?->manufacturer?->name,
|
||||
'company' => $asset->company?->name,
|
||||
'location' => $asset->location?->name,
|
||||
'assigned_to_id' => $asset->assigned_to,
|
||||
'assigned_to_type' => $asset->assigned_type ? class_basename($asset->assigned_type) : null,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_assets', ['total' => $total, 'count' => count($assetsData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'assets' => $assetsData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()
|
||||
->description('Keyword to search across asset tag, serial, name, and model'),
|
||||
'status_type' => $schema->string()
|
||||
->description('Filter by status type: RTD (ready to deploy), Deployed, Archived, Pending, or Undeployable'),
|
||||
'company_id' => $schema->number()
|
||||
->description('Filter by company ID'),
|
||||
'location_id' => $schema->number()
|
||||
->description('Filter by location ID'),
|
||||
'category_id' => $schema->number()
|
||||
->description('Filter by category ID'),
|
||||
'model_id' => $schema->number()
|
||||
->description('Filter by model ID'),
|
||||
'manufacturer_id' => $schema->number()
|
||||
->description('Filter by manufacturer ID'),
|
||||
'limit' => $schema->number()
|
||||
->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()
|
||||
->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching assets')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_categories')]
|
||||
#[Title('List Categories')]
|
||||
#[Description('Search and list Snipe-IT categories with optional filtering by type and pagination')]
|
||||
class ListCategoriesTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Category::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'category_type' => 'nullable|string|in:asset,accessory,consumable,component,license',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$categories = Category::withCount(
|
||||
'showableAssets as assets_count',
|
||||
'accessories as accessories_count',
|
||||
'consumables as consumables_count',
|
||||
'components as components_count',
|
||||
'licenses as licenses_count'
|
||||
);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$categories->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('category_type')) {
|
||||
$categories->where('category_type', $request->get('category_type'));
|
||||
}
|
||||
|
||||
$categories->orderBy('created_at', 'desc');
|
||||
|
||||
$total = $categories->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $categories->skip($offset)->take($limit)->get();
|
||||
|
||||
$categoriesData = $results->map(fn (Category $category) => [
|
||||
'id' => $category->id,
|
||||
'name' => $category->name,
|
||||
'category_type' => $category->category_type,
|
||||
'assets_count' => $category->assets_count,
|
||||
'accessories_count' => $category->accessories_count,
|
||||
'consumables_count' => $category->consumables_count,
|
||||
'components_count' => $category->components_count,
|
||||
'licenses_count' => $category->licenses_count,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_categories', ['total' => $total, 'count' => count($categoriesData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'categories' => $categoriesData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across category name, type, notes'),
|
||||
'category_type' => $schema->string()->description('Filter by type: asset, accessory, consumable, component, or license'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching categories')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'categories' => $schema->array()->description('List of categories')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_companies')]
|
||||
#[Title('List Companies')]
|
||||
#[Description('Search and list Snipe-IT companies with optional filtering and pagination')]
|
||||
class ListCompaniesTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Company::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$companies = Company::withCount([
|
||||
'assets as assets_count' => fn ($q) => $q->AssetsForShow(),
|
||||
])->withCount('licenses as licenses_count', 'users as users_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$companies->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
$companies->orderBy('created_at', 'desc');
|
||||
|
||||
$total = $companies->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $companies->skip($offset)->take($limit)->get();
|
||||
|
||||
$companiesData = $results->map(fn (Company $company) => [
|
||||
'id' => $company->id,
|
||||
'name' => $company->name,
|
||||
'phone' => $company->phone,
|
||||
'fax' => $company->fax,
|
||||
'email' => $company->email,
|
||||
'assets_count' => $company->assets_count,
|
||||
'licenses_count' => $company->licenses_count,
|
||||
'users_count' => $company->users_count,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_companies', ['total' => $total, 'count' => count($companiesData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'companies' => $companiesData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across company name, phone, fax, email'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching companies')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'companies' => $schema->array()->description('List of companies')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Consumable;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_consumables')]
|
||||
#[Title('List Consumables')]
|
||||
#[Description('Search and list Snipe-IT consumables with optional filtering by keyword, company, category, manufacturer, location, and pagination')]
|
||||
class ListConsumablesTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('index', Consumable::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'company_id' => 'nullable|integer',
|
||||
'category_id' => 'nullable|integer',
|
||||
'manufacturer_id' => 'nullable|integer',
|
||||
'location_id' => 'nullable|integer',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$consumables = Consumable::with('company', 'category', 'manufacturer', 'supplier', 'location')
|
||||
->withCount('users as users_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$consumables->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$consumables->where('consumables.company_id', $request->get('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$consumables->where('category_id', $request->get('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('manufacturer_id')) {
|
||||
$consumables->where('manufacturer_id', $request->get('manufacturer_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$consumables->where('location_id', $request->get('location_id'));
|
||||
}
|
||||
|
||||
$total = $consumables->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $consumables->orderBy('consumables.created_at', 'desc')->skip($offset)->take($limit)->get();
|
||||
|
||||
$consumablesData = $results->map(fn (Consumable $consumable) => [
|
||||
'id' => $consumable->id,
|
||||
'name' => $consumable->name,
|
||||
'qty' => $consumable->qty,
|
||||
'users_count' => $consumable->users_count,
|
||||
'category' => $consumable->category?->name,
|
||||
'manufacturer' => $consumable->manufacturer?->name,
|
||||
'company' => $consumable->company?->name,
|
||||
'location' => $consumable->location?->name,
|
||||
'purchase_cost' => $consumable->purchase_cost,
|
||||
'purchase_date' => $consumable->purchase_date?->format('Y-m-d'),
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_consumables', ['total' => $total, 'count' => count($consumablesData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'consumables' => $consumablesData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across consumable name and other fields'),
|
||||
'company_id' => $schema->number()->description('Filter by company ID'),
|
||||
'category_id' => $schema->number()->description('Filter by category ID'),
|
||||
'manufacturer_id' => $schema->number()->description('Filter by manufacturer ID'),
|
||||
'location_id' => $schema->number()->description('Filter by location ID'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching consumables')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Depreciation;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_depreciations')]
|
||||
#[Title('List Depreciations')]
|
||||
#[Description('Search and list Snipe-IT depreciation schedules with optional filtering and pagination')]
|
||||
class ListDepreciationsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Depreciation::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$depreciations = Depreciation::withCount('models as models_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$depreciations->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
$depreciations->orderBy('created_at', 'desc');
|
||||
|
||||
$total = $depreciations->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $depreciations->skip($offset)->take($limit)->get();
|
||||
|
||||
$depreciationsData = $results->map(fn (Depreciation $dep) => [
|
||||
'id' => $dep->id,
|
||||
'name' => $dep->name,
|
||||
'months' => $dep->months,
|
||||
'models_count' => $dep->models_count,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_depreciations', ['total' => $total, 'count' => count($depreciationsData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'depreciations' => $depreciationsData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across depreciation name'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching depreciations')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'depreciations' => $schema->array()->description('List of depreciation schedules')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_groups')]
|
||||
#[Title('List Groups')]
|
||||
#[Description('List Snipe-IT permission groups with optional search and pagination')]
|
||||
class ListGroupsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('superadmin')) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$groups = Group::withCount('users as users_count')
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$groups->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
$total = $groups->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $groups->skip($offset)->take($limit)->get();
|
||||
|
||||
$groupsData = $results->map(fn (Group $group) => [
|
||||
'id' => $group->id,
|
||||
'name' => $group->name,
|
||||
'notes' => $group->notes,
|
||||
'users_count' => $group->users_count,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_groups', ['total' => $total, 'count' => count($groupsData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'groups' => $groupsData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search groups by name or notes'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching groups')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'groups' => $schema->array()->description('List of groups')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Component;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\License;
|
||||
use App\Models\Location;
|
||||
use App\Models\Maintenance;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_history')]
|
||||
#[Title('List History')]
|
||||
#[Description('List the activity history for a Snipe-IT object. Supported types: accessory, asset, asset_model, component, consumable, license, location, maintenance, user')]
|
||||
class ListHistoryTool extends Tool
|
||||
{
|
||||
private const TYPE_MAP = [
|
||||
'accessory' => Accessory::class,
|
||||
'asset' => Asset::class,
|
||||
'asset_model' => AssetModel::class,
|
||||
'component' => Component::class,
|
||||
'consumable' => Consumable::class,
|
||||
'license' => License::class,
|
||||
'location' => Location::class,
|
||||
'maintenance' => Maintenance::class,
|
||||
'user' => User::class,
|
||||
];
|
||||
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$validTypes = implode(',', array_keys(self::TYPE_MAP));
|
||||
|
||||
$request->validate([
|
||||
'object_type' => 'required|string|in:'.$validTypes,
|
||||
'id' => 'required|integer|min:1',
|
||||
'search' => 'nullable|string|max:255',
|
||||
'action_type' => 'nullable|string|max:100',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$objectType = $request->get('object_type');
|
||||
$modelClass = self::TYPE_MAP[$objectType];
|
||||
|
||||
$object = $modelClass::withTrashed()->find($request->get('id'));
|
||||
|
||||
if (! $object) {
|
||||
return Response::make(Response::error(trans('mcp.object_not_found', ['type' => $objectType])));
|
||||
}
|
||||
|
||||
if (! Gate::allows('history', $object)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$modelClass = get_class($object);
|
||||
$modelId = $object->getKey();
|
||||
|
||||
// Wrap the item/target OR in a subquery so additional filters apply to both sides.
|
||||
$history = Actionlog::where(function ($q) use ($modelClass, $modelId) {
|
||||
$q->where('item_type', $modelClass)
|
||||
->where('item_id', $modelId)
|
||||
->orWhere(function ($q2) use ($modelClass, $modelId) {
|
||||
$q2->where('target_type', $modelClass)
|
||||
->where('target_id', $modelId);
|
||||
});
|
||||
});
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$history->TextSearch(e($request->get('search')));
|
||||
}
|
||||
|
||||
if ($request->filled('action_type')) {
|
||||
$history->where('action_type', $request->get('action_type'));
|
||||
}
|
||||
|
||||
$history->orderBy('action_logs.created_at', 'desc');
|
||||
|
||||
$total = (clone $history)->count();
|
||||
$records = $history->skip($offset)->take($limit)->forApiHistory()->get();
|
||||
|
||||
$entries = $records->map(fn ($log) => [
|
||||
'id' => $log->id,
|
||||
'action_type' => $log->action_type,
|
||||
'created_at' => $log->created_at?->toISOString(),
|
||||
'note' => $log->note,
|
||||
'created_by' => $log->adminuser ? [
|
||||
'id' => $log->adminuser->id,
|
||||
'username' => $log->adminuser->username,
|
||||
] : null,
|
||||
'target' => $log->target ? [
|
||||
'id' => $log->target->getKey(),
|
||||
'type' => class_basename($log->target_type),
|
||||
'name' => $log->target->present()->name() ?? null,
|
||||
] : null,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_history', [
|
||||
'total' => $total,
|
||||
'count' => count($entries),
|
||||
'type' => $objectType,
|
||||
]))
|
||||
)->withStructuredContent([
|
||||
'object_type' => $objectType,
|
||||
'object_id' => $object->id,
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'history' => $entries,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'object_type' => $schema->string()->description('Type of object: accessory, asset, asset_model, component, consumable, license, location, maintenance, user'),
|
||||
'id' => $schema->number()->description('Numeric ID of the object'),
|
||||
'search' => $schema->string()->description('Filter history by keyword'),
|
||||
'action_type' => $schema->string()->description('Filter by action type (e.g. checkout, checkin, update, note added, uploaded)'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'object_type' => $schema->string()->description('Type of object queried')->required(),
|
||||
'object_id' => $schema->number()->description('ID of the object queried')->required(),
|
||||
'total' => $schema->number()->description('Total number of history entries')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'history' => $schema->array()->description('List of history entries'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\License;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_licenses')]
|
||||
#[Title('List Licenses')]
|
||||
#[Description('Search and list Snipe-IT licenses with optional filtering by keyword, company, category, and manufacturer')]
|
||||
class ListLicensesTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('index', License::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'company_id' => 'nullable|integer',
|
||||
'category_id' => 'nullable|integer',
|
||||
'manufacturer_id' => 'nullable|integer',
|
||||
'supplier_id' => 'nullable|integer',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$licenses = License::with('company', 'manufacturer', 'supplier', 'category')
|
||||
->withCount('freeSeats as free_seats_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$licenses->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$licenses->where('licenses.company_id', '=', $request->get('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$licenses->where('category_id', '=', $request->get('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('manufacturer_id')) {
|
||||
$licenses->where('manufacturer_id', '=', $request->get('manufacturer_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('supplier_id')) {
|
||||
$licenses->where('supplier_id', '=', $request->get('supplier_id'));
|
||||
}
|
||||
|
||||
$licenses->orderBy('licenses.created_at', 'desc');
|
||||
|
||||
$total = $licenses->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $licenses->skip($offset)->take($limit)->get();
|
||||
|
||||
$licensesData = $results->map(fn (License $license) => [
|
||||
'id' => $license->id,
|
||||
'name' => $license->name,
|
||||
'serial' => $license->serial,
|
||||
'seats' => $license->seats,
|
||||
'free_seats' => $license->free_seats_count,
|
||||
'category' => $license->category?->name,
|
||||
'manufacturer' => $license->manufacturer?->name,
|
||||
'company' => $license->company?->name,
|
||||
'supplier' => $license->supplier?->name,
|
||||
'expiration_date' => $license->expiration_date?->format('Y-m-d'),
|
||||
'purchase_date' => $license->purchase_date?->format('Y-m-d'),
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_licenses', ['total' => $total, 'count' => count($licensesData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'licenses' => $licensesData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across name, serial, notes, and order number'),
|
||||
'company_id' => $schema->number()->description('Filter by company ID'),
|
||||
'category_id' => $schema->number()->description('Filter by category ID'),
|
||||
'manufacturer_id' => $schema->number()->description('Filter by manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Filter by supplier ID'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching licenses')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Location;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_locations')]
|
||||
#[Title('List Locations')]
|
||||
#[Description('Search and list Snipe-IT locations with optional filtering and pagination')]
|
||||
class ListLocationsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Location::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'parent_id' => 'nullable|integer',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$locations = Location::with('parent')->withCount(
|
||||
'assets as assets_count',
|
||||
'users as users_count',
|
||||
'children as children_count'
|
||||
);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$locations->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('parent_id')) {
|
||||
$locations->where('parent_id', $request->get('parent_id'));
|
||||
}
|
||||
|
||||
$locations->orderBy('created_at', 'desc');
|
||||
|
||||
$total = $locations->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $locations->skip($offset)->take($limit)->get();
|
||||
|
||||
$locationsData = $results->map(fn (Location $location) => [
|
||||
'id' => $location->id,
|
||||
'name' => $location->name,
|
||||
'address' => $location->address,
|
||||
'city' => $location->city,
|
||||
'state' => $location->state,
|
||||
'country' => $location->country,
|
||||
'zip' => $location->zip,
|
||||
'phone' => $location->phone,
|
||||
'parent_id' => $location->parent_id,
|
||||
'parent' => $location->parent?->name,
|
||||
'assets_count' => $location->assets_count,
|
||||
'users_count' => $location->users_count,
|
||||
'children_count' => $location->children_count,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_locations', ['total' => $total, 'count' => count($locationsData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'locations' => $locationsData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across location name, city, state, country'),
|
||||
'parent_id' => $schema->number()->description('Filter by parent location ID'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching locations')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'locations' => $schema->array()->description('List of locations')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Maintenance;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_maintenances')]
|
||||
#[Title('List Maintenances')]
|
||||
#[Description('List asset maintenances with optional filtering by asset and pagination')]
|
||||
class ListMaintenancesTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'asset_id' => 'nullable|integer',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$maintenances = Maintenance::with('asset', 'supplier');
|
||||
|
||||
if ($request->filled('asset_id')) {
|
||||
$maintenances->where('asset_id', $request->get('asset_id'));
|
||||
}
|
||||
|
||||
$maintenances->orderBy('created_at', 'desc');
|
||||
|
||||
$total = $maintenances->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $maintenances->skip($offset)->take($limit)->get();
|
||||
|
||||
$maintenancesData = $results->map(fn (Maintenance $maintenance) => [
|
||||
'id' => $maintenance->id,
|
||||
'title' => $maintenance->name,
|
||||
'asset_id' => $maintenance->asset_id,
|
||||
'asset_tag' => $maintenance->asset?->asset_tag,
|
||||
'is_warranty' => (bool) $maintenance->is_warranty,
|
||||
'cost' => $maintenance->cost,
|
||||
'start_date' => $maintenance->start_date,
|
||||
'completion_date' => $maintenance->completion_date,
|
||||
'supplier' => $maintenance->supplier?->name,
|
||||
'notes' => $maintenance->notes,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_maintenances', ['total' => $total, 'count' => count($maintenancesData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'maintenances' => $maintenancesData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_id' => $schema->number()->description('Filter by asset ID'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching maintenances')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'maintenances' => $schema->array()->description('List of maintenances')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Manufacturer;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_manufacturers')]
|
||||
#[Title('List Manufacturers')]
|
||||
#[Description('Search and list Snipe-IT manufacturers with optional filtering and pagination')]
|
||||
class ListManufacturersTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Manufacturer::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$manufacturers = Manufacturer::withCount(
|
||||
'assets as assets_count',
|
||||
'licenses as licenses_count',
|
||||
'accessories as accessories_count',
|
||||
'components as components_count'
|
||||
);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$manufacturers->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
$manufacturers->orderBy('created_at', 'desc');
|
||||
|
||||
$total = $manufacturers->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $manufacturers->skip($offset)->take($limit)->get();
|
||||
|
||||
$manufacturersData = $results->map(fn (Manufacturer $manufacturer) => [
|
||||
'id' => $manufacturer->id,
|
||||
'name' => $manufacturer->name,
|
||||
'url' => $manufacturer->url,
|
||||
'support_url' => $manufacturer->support_url,
|
||||
'support_email' => $manufacturer->support_email,
|
||||
'support_phone' => $manufacturer->support_phone,
|
||||
'assets_count' => $manufacturer->assets_count,
|
||||
'licenses_count' => $manufacturer->licenses_count,
|
||||
'accessories_count' => $manufacturer->accessories_count,
|
||||
'components_count' => $manufacturer->components_count,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_manufacturers', ['total' => $total, 'count' => count($manufacturersData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'manufacturers' => $manufacturersData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across manufacturer name and notes'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching manufacturers')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'manufacturers' => $schema->array()->description('List of manufacturers')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Statuslabel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_status_labels')]
|
||||
#[Title('List Status Labels')]
|
||||
#[Description('Search and list Snipe-IT status labels with optional filtering and pagination')]
|
||||
class ListStatusLabelsTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Statuslabel::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'status_type' => 'nullable|string|in:deployable,pending,archived,undeployable',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$labels = Statuslabel::withCount('assets as assets_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$labels->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('status_type')) {
|
||||
$type = $request->get('status_type');
|
||||
if ($type === 'deployable') {
|
||||
$labels->Deployable();
|
||||
} elseif ($type === 'pending') {
|
||||
$labels->Pending();
|
||||
} elseif ($type === 'archived') {
|
||||
$labels->Archived();
|
||||
} elseif ($type === 'undeployable') {
|
||||
$labels->Undeployable();
|
||||
}
|
||||
}
|
||||
|
||||
$total = $labels->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $labels->skip($offset)->take($limit)->get();
|
||||
|
||||
$labelsData = $results->map(fn (Statuslabel $label) => [
|
||||
'id' => $label->id,
|
||||
'name' => $label->name,
|
||||
'type' => $label->getStatuslabelType(),
|
||||
'color' => $label->color,
|
||||
'assets_count' => $label->assets_count,
|
||||
'deployable' => $label->deployable,
|
||||
'pending' => $label->pending,
|
||||
'archived' => $label->archived,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_status_labels', ['total' => $total, 'count' => count($labelsData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'status_labels' => $labelsData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across status label name and notes'),
|
||||
'status_type' => $schema->string()->description('Filter by type: deployable, pending, archived, undeployable'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching status labels')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'status_labels' => $schema->array()->description('List of status labels')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Supplier;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_suppliers')]
|
||||
#[Title('List Suppliers')]
|
||||
#[Description('Search and list Snipe-IT suppliers with optional filtering and pagination')]
|
||||
class ListSuppliersTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('view', Supplier::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$suppliers = Supplier::withCount(
|
||||
'assets as assets_count',
|
||||
'licenses as licenses_count'
|
||||
);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$suppliers->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
$suppliers->orderBy('created_at', 'desc');
|
||||
|
||||
$total = $suppliers->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $suppliers->skip($offset)->take($limit)->get();
|
||||
|
||||
$suppliersData = $results->map(fn (Supplier $supplier) => [
|
||||
'id' => $supplier->id,
|
||||
'name' => $supplier->name,
|
||||
'address' => $supplier->address,
|
||||
'city' => $supplier->city,
|
||||
'state' => $supplier->state,
|
||||
'country' => $supplier->country,
|
||||
'phone' => $supplier->phone,
|
||||
'email' => $supplier->email,
|
||||
'url' => $supplier->url,
|
||||
'assets_count' => $supplier->assets_count,
|
||||
'licenses_count' => $supplier->licenses_count,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_suppliers', ['total' => $total, 'count' => count($suppliersData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'suppliers' => $suppliersData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across supplier fields'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching suppliers')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'suppliers' => $schema->array()->description('List of suppliers')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Company;
|
||||
use App\Models\Component;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\Department;
|
||||
use App\Models\License;
|
||||
use App\Models\Location;
|
||||
use App\Models\Maintenance;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_uploads')]
|
||||
#[Title('List Uploads')]
|
||||
#[Description('List files uploaded to a Snipe-IT object. Supported types: accessories, assets, companies, components, consumables, departments, licenses, locations, maintenances, models, suppliers, users')]
|
||||
class ListUploadsTool extends Tool
|
||||
{
|
||||
private const TYPE_MAP = [
|
||||
'accessories' => Accessory::class,
|
||||
'assets' => Asset::class,
|
||||
'companies' => Company::class,
|
||||
'components' => Component::class,
|
||||
'consumables' => Consumable::class,
|
||||
'departments' => Department::class,
|
||||
'licenses' => License::class,
|
||||
'locations' => Location::class,
|
||||
'maintenances' => Maintenance::class,
|
||||
'models' => AssetModel::class,
|
||||
'suppliers' => Supplier::class,
|
||||
'users' => User::class,
|
||||
];
|
||||
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$validTypes = implode(',', array_keys(self::TYPE_MAP));
|
||||
|
||||
$request->validate([
|
||||
'object_type' => 'required|string|in:'.$validTypes,
|
||||
'id' => 'required|integer|min:1',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$objectType = $request->get('object_type');
|
||||
$modelClass = self::TYPE_MAP[$objectType];
|
||||
|
||||
$object = $modelClass::withTrashed()->find($request->get('id'));
|
||||
|
||||
if (! $object) {
|
||||
return Response::make(Response::error(trans('mcp.object_not_found', ['type' => $objectType])));
|
||||
}
|
||||
|
||||
if (! Gate::allows('files', $object)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$query = $object->uploads()->with('adminuser');
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$uploads = $query->skip($offset)->take($limit)->orderBy('created_at', 'desc')->get();
|
||||
|
||||
$files = $uploads->map(fn ($file) => [
|
||||
'id' => $file->id,
|
||||
'filename' => $file->filename,
|
||||
'url' => $file->uploads_file_url(),
|
||||
'note' => $file->note,
|
||||
'created_by' => $file->adminuser ? [
|
||||
'id' => $file->adminuser->id,
|
||||
'username' => $file->adminuser->username,
|
||||
] : null,
|
||||
'created_at' => $file->created_at?->toISOString(),
|
||||
'exists_on_disk' => Storage::exists($file->uploads_file_path()),
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_uploads', [
|
||||
'total' => $total,
|
||||
'count' => count($files),
|
||||
'type' => $objectType,
|
||||
]))
|
||||
)->withStructuredContent([
|
||||
'object_type' => $objectType,
|
||||
'object_id' => $object->id,
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'files' => $files,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'object_type' => $schema->string()->description('Type of object: accessories, assets, companies, components, consumables, departments, licenses, locations, maintenances, models, suppliers, users'),
|
||||
'id' => $schema->number()->description('Numeric ID of the object'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'object_type' => $schema->string()->description('Type of object queried')->required(),
|
||||
'object_id' => $schema->number()->description('ID of the object queried')->required(),
|
||||
'total' => $schema->number()->description('Total number of uploaded files')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'files' => $schema->array()->description('List of uploaded files'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_users')]
|
||||
#[Title('List Users')]
|
||||
#[Description('Search and list Snipe-IT users with optional filtering by keyword, company, department, and location')]
|
||||
class ListUsersTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('index', User::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'search' => 'nullable|string|max:255',
|
||||
'company_id' => 'nullable|integer',
|
||||
'department_id' => 'nullable|integer',
|
||||
'location_id' => 'nullable|integer',
|
||||
'activated' => 'nullable|boolean',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$users = User::with('company', 'department', 'userloc', 'manager')
|
||||
->withCount(['assets as assets_count', 'licenses as licenses_count']);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$users->TextSearch($request->get('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$users->where('users.company_id', '=', $request->get('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('department_id')) {
|
||||
$users->where('users.department_id', '=', $request->get('department_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$users->where('users.location_id', '=', $request->get('location_id'));
|
||||
}
|
||||
|
||||
if ($request->has('activated')) {
|
||||
$users->where('users.activated', '=', $request->get('activated'));
|
||||
}
|
||||
|
||||
$users->orderBy('users.last_name', 'asc')->orderBy('users.first_name', 'asc');
|
||||
|
||||
$total = $users->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $users->skip($offset)->take($limit)->get();
|
||||
|
||||
$usersData = $results->map(fn (User $user) => [
|
||||
'id' => $user->id,
|
||||
'first_name' => $user->first_name,
|
||||
'last_name' => $user->last_name,
|
||||
'username' => $user->username,
|
||||
'email' => $user->email,
|
||||
'jobtitle' => $user->jobtitle,
|
||||
'company' => $user->company?->name,
|
||||
'department' => $user->department?->name,
|
||||
'location' => $user->userloc?->name,
|
||||
'manager' => $user->manager ? trim($user->manager->first_name.' '.$user->manager->last_name) : null,
|
||||
'activated' => (bool) $user->activated,
|
||||
'assets_count' => $user->assets_count,
|
||||
'licenses_count' => $user->licenses_count,
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_users', ['total' => $total, 'count' => count($usersData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'users' => $usersData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'search' => $schema->string()->description('Keyword to search across name, username, email, and employee number'),
|
||||
'company_id' => $schema->number()->description('Filter by company ID'),
|
||||
'department_id' => $schema->number()->description('Filter by department ID'),
|
||||
'location_id' => $schema->number()->description('Filter by location ID'),
|
||||
'activated' => $schema->boolean()->description('Filter by account activated status'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching users')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('reset_2fa')]
|
||||
#[Title('Reset Two-Factor Authentication')]
|
||||
#[Description('Reset two-factor authentication for a Snipe-IT user')]
|
||||
class Reset2FATool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('update', User::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$user = User::find($request->get('id'));
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
|
||||
$user->two_factor_secret = null;
|
||||
$user->two_factor_enrolled = 0;
|
||||
$user->two_factor_optin = 0;
|
||||
$user->save();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.two_factor_reset', ['username' => $user->username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.two_factor_reset', ['username' => $user->username]),
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the user whose 2FA should be reset (required)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the reset succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the user'),
|
||||
'username' => $schema->string()->description('Username of the user'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('restore_asset')]
|
||||
#[Title('Restore Asset')]
|
||||
#[Description('Restore a soft-deleted Snipe-IT asset')]
|
||||
class RestoreAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$asset = Asset::withTrashed()->find($request->get('id'));
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! $asset->deleted_at) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_deleted')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$asset->restore();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_restored', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_restored', ['asset_tag' => $asset->asset_tag]),
|
||||
'id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the asset to restore (required)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the restore succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the restored asset'),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the restored asset'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('restore_user')]
|
||||
#[Title('Restore User')]
|
||||
#[Description('Restore a soft-deleted Snipe-IT user')]
|
||||
class RestoreUserTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('delete', User::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$user = User::withTrashed()->find($request->get('id'));
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
|
||||
if (! $user->deleted_at) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_deleted')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', User::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$user->restore();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.user_restored', ['username' => $user->username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.user_restored', ['username' => $user->username]),
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the user to restore (required)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the restore succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the restored user'),
|
||||
'username' => $schema->string()->description('Username of the restored user'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('send_password_reset')]
|
||||
#[Title('Send Password Reset Email')]
|
||||
#[Description('Send a password reset link to a Snipe-IT user identified by numeric ID, username, or email address. The user must be active, have an email address, and not be an LDAP-imported account.')]
|
||||
class SendPasswordResetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'username' => 'nullable|string|max:191',
|
||||
'email' => 'nullable|string|max:191',
|
||||
]);
|
||||
|
||||
$user = $this->resolveUser($request);
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', $user)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if (! $user->activated) {
|
||||
return Response::make(Response::error(trans('mcp.password_reset_user_inactive', ['username' => $user->username])));
|
||||
}
|
||||
|
||||
if (empty($user->email)) {
|
||||
return Response::make(Response::error(trans('mcp.password_reset_no_email', ['username' => $user->username])));
|
||||
}
|
||||
|
||||
if ($user->ldap_import) {
|
||||
return Response::make(Response::error(trans('mcp.password_reset_ldap_user', ['username' => $user->username])));
|
||||
}
|
||||
|
||||
try {
|
||||
$result = Password::sendResetLink(['email' => trim($user->email)]);
|
||||
} catch (\Exception $e) {
|
||||
return Response::make(Response::error(trans('mcp.password_reset_send_failed', ['error' => $e->getMessage()])));
|
||||
}
|
||||
|
||||
if ($result === Password::RESET_LINK_SENT) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.password_reset_sent', ['email' => $user->email]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.password_reset_sent', ['email' => $user->email]),
|
||||
'username' => $user->username,
|
||||
'email' => $user->email,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.password_reset_send_failed', ['error' => $result])));
|
||||
}
|
||||
|
||||
private function resolveUser(Request $request): ?User
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return User::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('username')) {
|
||||
return User::where('username', $request->get('username'))->first();
|
||||
}
|
||||
if ($request->filled('email')) {
|
||||
return User::where('email', $request->get('email'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the user'),
|
||||
'username' => $schema->string()->description('Username of the user'),
|
||||
'email' => $schema->string()->description('Email address of the user'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the reset email was sent'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'username' => $schema->string()->description('Username of the user'),
|
||||
'email' => $schema->string()->description('Email address the reset link was sent to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\AssetModel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('show_asset_model')]
|
||||
#[Title('Show Asset Model Details')]
|
||||
#[Description('Look up a single Snipe-IT asset model by numeric ID or name and return its full details')]
|
||||
class ShowAssetModelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$model = $this->resolveModel($request);
|
||||
|
||||
if ($model === false) {
|
||||
return Response::make(Response::error(trans('mcp.id_or_name_required')));
|
||||
}
|
||||
|
||||
if (! $model) {
|
||||
return Response::make(Response::error(trans('mcp.asset_model_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', $model)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$model->loadCount('assets as assets_count');
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_model_found', ['name' => $model->name]))
|
||||
)->withStructuredContent([
|
||||
'id' => $model->id,
|
||||
'name' => $model->name,
|
||||
'model_number' => $model->model_number,
|
||||
'category_id' => $model->category_id,
|
||||
'category' => $model->category?->name,
|
||||
'manufacturer_id' => $model->manufacturer_id,
|
||||
'manufacturer' => $model->manufacturer?->name,
|
||||
'depreciation_id' => $model->depreciation_id,
|
||||
'depreciation' => $model->depreciation?->name,
|
||||
'assets_count' => $model->assets_count,
|
||||
'eol' => $model->eol,
|
||||
'min_amt' => $model->min_amt,
|
||||
'notes' => $model->notes,
|
||||
'created_at' => $model->created_at?->format('Y-m-d H:i:s'),
|
||||
'updated_at' => $model->updated_at?->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveModel(Request $request): AssetModel|false|null
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return AssetModel::with('category', 'manufacturer', 'depreciation')->find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return AssetModel::with('category', 'manufacturer', 'depreciation')->where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the asset model to look up'),
|
||||
'name' => $schema->string()->description('Name of the asset model to look up'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric asset model ID'),
|
||||
'name' => $schema->string()->description('Asset model name'),
|
||||
'model_number' => $schema->string()->description('Model number'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
'category' => $schema->string()->description('Category name'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'manufacturer' => $schema->string()->description('Manufacturer name'),
|
||||
'depreciation_id' => $schema->number()->description('Depreciation schedule ID'),
|
||||
'depreciation' => $schema->string()->description('Depreciation schedule name'),
|
||||
'assets_count' => $schema->number()->description('Number of assets using this model'),
|
||||
'eol' => $schema->number()->description('End of life in months'),
|
||||
'min_amt' => $schema->number()->description('Minimum quantity alert threshold'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'created_at' => $schema->string()->description('Creation timestamp'),
|
||||
'updated_at' => $schema->string()->description('Last update timestamp'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('show_asset')]
|
||||
#[Title('Show Asset Details')]
|
||||
#[Description('Look up a single Snipe-IT asset by asset tag or numeric ID and return its full details')]
|
||||
class ShowAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'id' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$with = ['status', 'model.category', 'model.manufacturer', 'location', 'defaultLoc', 'company', 'supplier', 'adminuser'];
|
||||
$asset = null;
|
||||
|
||||
if ($request->filled('asset_tag')) {
|
||||
$asset = Asset::where('asset_tag', $request->get('asset_tag'))->with($with)->first();
|
||||
} elseif ($request->filled('serial')) {
|
||||
$asset = Asset::where('serial', $request->get('serial'))->with($with)->first();
|
||||
} elseif ($request->filled('id')) {
|
||||
$asset = Asset::with($with)->find($request->get('id'));
|
||||
}
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(
|
||||
Response::error(trans('mcp.asset_not_found'))
|
||||
);
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_found', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent($this->formatAsset($asset));
|
||||
}
|
||||
|
||||
private function formatAsset(Asset $asset): array
|
||||
{
|
||||
return [
|
||||
'id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'name' => $asset->name,
|
||||
'serial' => $asset->serial,
|
||||
'status' => $asset->status?->name,
|
||||
'status_type' => $asset->status?->getStatuslabelType(),
|
||||
'model' => $asset->model?->name,
|
||||
'model_number' => $asset->model?->model_number,
|
||||
'category' => $asset->model?->category?->name,
|
||||
'manufacturer' => $asset->model?->manufacturer?->name,
|
||||
'company' => $asset->company?->name,
|
||||
'location' => $asset->location?->name,
|
||||
'rtd_location' => $asset->defaultLoc?->name,
|
||||
'supplier' => $asset->supplier?->name,
|
||||
'assigned_to_id' => $asset->assigned_to,
|
||||
'assigned_to_type' => $asset->assigned_type ? class_basename($asset->assigned_type) : null,
|
||||
'notes' => $asset->notes,
|
||||
'order_number' => $asset->order_number,
|
||||
'purchase_date' => $asset->purchase_date?->format('Y-m-d'),
|
||||
'purchase_cost' => $asset->purchase_cost,
|
||||
'warranty_months' => $asset->warranty_months,
|
||||
'last_checkout' => $asset->last_checkout,
|
||||
'last_checkin' => $asset->last_checkin,
|
||||
'expected_checkin' => $asset->expected_checkin,
|
||||
'last_audit_date' => $asset->last_audit_date,
|
||||
'created_at' => $asset->created_at?->format('Y-m-d H:i:s'),
|
||||
'updated_at' => $asset->updated_at?->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()
|
||||
->description('The asset tag of the asset to look up'),
|
||||
'serial' => $schema->string()
|
||||
->description('The serial number of the asset to look up'),
|
||||
'id' => $schema->number()
|
||||
->description('The numeric ID of the asset to look up'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric asset ID'),
|
||||
'asset_tag' => $schema->string()->description('Asset tag'),
|
||||
'name' => $schema->string()->description('Asset name'),
|
||||
'serial' => $schema->string()->description('Serial number'),
|
||||
'status' => $schema->string()->description('Status label name'),
|
||||
'status_type' => $schema->string()->description('Status type: deployable, pending, or archived'),
|
||||
'model' => $schema->string()->description('Asset model name'),
|
||||
'model_number' => $schema->string()->description('Model number'),
|
||||
'category' => $schema->string()->description('Category name'),
|
||||
'manufacturer' => $schema->string()->description('Manufacturer name'),
|
||||
'company' => $schema->string()->description('Company name'),
|
||||
'location' => $schema->string()->description('Current location name'),
|
||||
'rtd_location' => $schema->string()->description('Default return-to-deploy location name'),
|
||||
'assigned_to_id' => $schema->number()->description('ID of the entity this asset is currently assigned to'),
|
||||
'assigned_to_type' => $schema->string()->description('Type of entity assigned to: User, Asset, or Location'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'purchase_cost' => $schema->string()->description('Purchase cost'),
|
||||
'last_checkout' => $schema->string()->description('Date of last checkout'),
|
||||
'last_checkin' => $schema->string()->description('Date of last checkin'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('show_category')]
|
||||
#[Title('Show Category Details')]
|
||||
#[Description('Look up a single Snipe-IT category by numeric ID or name and return its full details')]
|
||||
class ShowCategoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$withCounts = [
|
||||
'showableAssets as assets_count',
|
||||
'accessories as accessories_count',
|
||||
'consumables as consumables_count',
|
||||
'components as components_count',
|
||||
'licenses as licenses_count',
|
||||
];
|
||||
|
||||
$category = null;
|
||||
|
||||
if ($request->filled('id')) {
|
||||
$category = Category::withCount($withCounts)->find($request->get('id'));
|
||||
} elseif ($request->filled('name')) {
|
||||
$category = Category::withCount($withCounts)->where('name', $request->get('name'))->first();
|
||||
} else {
|
||||
return Response::make(Response::error(trans('mcp.id_or_name_required')));
|
||||
}
|
||||
|
||||
if (! $category) {
|
||||
return Response::make(Response::error(trans('mcp.category_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', $category)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.category_found', ['name' => $category->name]))
|
||||
)->withStructuredContent([
|
||||
'id' => $category->id,
|
||||
'name' => $category->name,
|
||||
'category_type' => $category->category_type,
|
||||
'assets_count' => $category->assets_count,
|
||||
'accessories_count' => $category->accessories_count,
|
||||
'consumables_count' => $category->consumables_count,
|
||||
'components_count' => $category->components_count,
|
||||
'licenses_count' => $category->licenses_count,
|
||||
'notes' => $category->notes,
|
||||
'created_at' => $category->created_at?->format('Y-m-d H:i:s'),
|
||||
'updated_at' => $category->updated_at?->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the category to look up'),
|
||||
'name' => $schema->string()->description('Name of the category to look up'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric category ID'),
|
||||
'name' => $schema->string()->description('Category name'),
|
||||
'category_type' => $schema->string()->description('Category type: asset, accessory, consumable, component, or license'),
|
||||
'assets_count' => $schema->number()->description('Number of assets in this category'),
|
||||
'accessories_count' => $schema->number()->description('Number of accessories in this category'),
|
||||
'consumables_count' => $schema->number()->description('Number of consumables in this category'),
|
||||
'components_count' => $schema->number()->description('Number of components in this category'),
|
||||
'licenses_count' => $schema->number()->description('Number of licenses in this category'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'created_at' => $schema->string()->description('Creation timestamp'),
|
||||
'updated_at' => $schema->string()->description('Last update timestamp'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('show_company')]
|
||||
#[Title('Show Company Details')]
|
||||
#[Description('Look up a single Snipe-IT company by numeric ID or name and return its full details')]
|
||||
class ShowCompanyTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$company = null;
|
||||
|
||||
if ($request->filled('id')) {
|
||||
$company = Company::withCount([
|
||||
'assets as assets_count' => fn ($q) => $q->AssetsForShow(),
|
||||
])->withCount('users as users_count')->find($request->get('id'));
|
||||
} elseif ($request->filled('name')) {
|
||||
$company = Company::withCount([
|
||||
'assets as assets_count' => fn ($q) => $q->AssetsForShow(),
|
||||
])->withCount('users as users_count')->where('name', $request->get('name'))->first();
|
||||
} else {
|
||||
return Response::make(Response::error(trans('mcp.id_or_name_required')));
|
||||
}
|
||||
|
||||
if (! $company) {
|
||||
return Response::make(Response::error(trans('mcp.company_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('view', $company)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.company_found', ['name' => $company->name]))
|
||||
)->withStructuredContent([
|
||||
'id' => $company->id,
|
||||
'name' => $company->name,
|
||||
'phone' => $company->phone,
|
||||
'fax' => $company->fax,
|
||||
'email' => $company->email,
|
||||
'assets_count' => $company->assets_count,
|
||||
'users_count' => $company->users_count,
|
||||
'notes' => $company->notes,
|
||||
'created_at' => $company->created_at?->format('Y-m-d H:i:s'),
|
||||
'updated_at' => $company->updated_at?->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the company to look up'),
|
||||
'name' => $schema->string()->description('Name of the company to look up'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric company ID'),
|
||||
'name' => $schema->string()->description('Company name'),
|
||||
'phone' => $schema->string()->description('Company phone number'),
|
||||
'fax' => $schema->string()->description('Company fax number'),
|
||||
'email' => $schema->string()->description('Company email address'),
|
||||
'assets_count' => $schema->number()->description('Number of assets belonging to this company'),
|
||||
'users_count' => $schema->number()->description('Number of users belonging to this company'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'created_at' => $schema->string()->description('Creation timestamp'),
|
||||
'updated_at' => $schema->string()->description('Last update timestamp'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user