Add a tool to update your own profile

This commit is contained in:
snipe
2026-05-08 14:58:08 +01:00
parent c75d0effe2
commit 8ccc705473
4 changed files with 197 additions and 2 deletions
+2
View File
@@ -93,6 +93,7 @@ use App\Mcp\Tools\UpdateLocationTool;
use App\Mcp\Tools\UpdateManufacturerTool;
use App\Mcp\Tools\UpdateStatusLabelTool;
use App\Mcp\Tools\UpdateSupplierTool;
use App\Mcp\Tools\UpdateProfileTool;
use App\Mcp\Tools\UpdateUserTool;
use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Attributes\Instructions;
@@ -124,6 +125,7 @@ class SnipeMCPServer extends Server
DeleteUserTool::class,
RestoreUserTool::class,
GetCurrentUserTool::class,
UpdateProfileTool::class,
GetUserAssetsTool::class,
Reset2FATool::class,
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Setting;
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_profile')]
#[Title('Update Profile')]
#[Description('Update the authenticated user\'s own profile information')]
class UpdateProfileTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'first_name' => 'nullable|string|max:255',
'last_name' => 'nullable|string|max:255',
'phone' => 'nullable|string|max:35',
'website' => 'nullable|url|max:255',
'gravatar' => 'nullable|string|max:255',
'locale' => 'nullable|string|max:10',
'two_factor_optin'=> 'nullable|boolean',
'location_id' => 'nullable|integer|exists:locations,id',
]);
$user = auth()->user();
if (Gate::allows('self.profile') && ! config('app.lock_passwords')) {
foreach (['first_name', 'last_name', 'phone', 'website', 'gravatar'] as $field) {
if ($request->filled($field)) {
$user->{$field} = $request->get($field);
}
}
}
if ($request->filled('locale')) {
$user->locale = $request->get('locale');
}
if (
$request->filled('two_factor_optin') &&
Gate::allows('self.two_factor') &&
Setting::getSettings()->two_factor_enabled == '1' &&
! config('app.lock_passwords')
) {
$user->two_factor_optin = $request->get('two_factor_optin');
}
if ($request->filled('location_id') && Gate::allows('self.edit_location') && ! config('app.lock_passwords')) {
$user->location_id = $request->get('location_id');
}
if ($user->save()) {
return Response::make(
Response::text('Profile updated successfully')
)->withStructuredContent([
'success' => true,
'message' => 'Profile updated successfully',
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'phone' => $user->phone,
'website' => $user->website,
'locale' => $user->locale,
'location_id'=> $user->location_id,
]);
}
return Response::make(Response::error('Update failed: '.$user->getErrors()->first()));
}
public function schema(JsonSchema $schema): array
{
return [
'first_name' => $schema->string()->description('First name'),
'last_name' => $schema->string()->description('Last name'),
'phone' => $schema->string()->description('Phone number'),
'website' => $schema->string()->description('Personal website URL'),
'gravatar' => $schema->string()->description('Gravatar email or hash'),
'locale' => $schema->string()->description('Locale/language code (e.g. en-US)'),
'two_factor_optin' => $schema->boolean()->description('Opt in to two-factor authentication (requires self.two_factor permission and 2FA enabled in settings)'),
'location_id' => $schema->number()->description('Default location ID (requires self.edit_location permission)'),
];
}
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(),
'first_name' => $schema->string()->description('Updated first name'),
'last_name' => $schema->string()->description('Updated last name'),
'phone' => $schema->string()->description('Updated phone number'),
'website' => $schema->string()->description('Updated website URL'),
'locale' => $schema->string()->description('Updated locale'),
'location_id' => $schema->number()->description('Updated location ID'),
];
}
}
+2 -2
View File
@@ -16,7 +16,7 @@
* list of urls, explode that out into an array to whitelist just those urls.
*/
$allowed_origins = env('CORS_ALLOWED_ORIGINS') !== null ?
explode(',', env('CORS_ALLOWED_ORIGINS')) : [];
explode(',', env('CORS_ALLOWED_ORIGINS')) : ['*'];
/**
* Original Laravel CORS package config file modifications end here
@@ -41,6 +41,6 @@ return [
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
'exposed_headers' => [],
'max_age' => 0,
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'paths' => ['api/*', 'sanctum/csrf-cookie', '.well-known/*', 'oauth/*', 'mcp/*'],
];
@@ -0,0 +1,87 @@
<?php
namespace Tests\Feature\Mcp;
use App\Mcp\Tools\UpdateProfileTool;
use App\Models\Location;
use App\Models\User;
use Laravel\Mcp\Request;
use Laravel\Mcp\ResponseFactory;
use Tests\TestCase;
class UpdateProfileToolTest extends TestCase
{
private User $user;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
$this->actingAs($this->user);
}
private function handle(array $args): ResponseFactory
{
return (new UpdateProfileTool)->handle(new Request($args));
}
public function test_updates_profile_fields()
{
$content = $this->handle([
'first_name' => 'Updated',
'last_name' => 'Name',
'phone' => '555-1234',
])->getStructuredContent();
$this->assertTrue($content['success']);
$this->assertDatabaseHas('users', [
'id' => $this->user->id,
'first_name' => 'Updated',
'last_name' => 'Name',
'phone' => '555-1234',
]);
}
public function test_updates_locale()
{
$content = $this->handle(['locale' => 'fr'])->getStructuredContent();
$this->assertTrue($content['success']);
$this->assertDatabaseHas('users', ['id' => $this->user->id, 'locale' => 'fr']);
}
public function test_updates_location_with_permission()
{
$this->actingAs(User::factory()->canEditOwnLocation()->create());
$location = Location::factory()->create();
$content = $this->handle(['location_id' => $location->id])->getStructuredContent();
$this->assertTrue($content['success']);
$this->assertEquals($location->id, $content['location_id']);
}
public function test_does_not_update_location_without_permission()
{
$location = Location::factory()->create();
$originalLocation = $this->user->location_id;
$this->handle(['location_id' => $location->id]);
$this->assertDatabaseHas('users', ['id' => $this->user->id, 'location_id' => $originalLocation]);
}
public function test_only_updates_provided_fields()
{
$originalLastName = $this->user->last_name;
$this->handle(['first_name' => 'Changed']);
$this->assertDatabaseHas('users', [
'id' => $this->user->id,
'first_name' => 'Changed',
'last_name' => $originalLastName,
]);
}
}