mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-06-11 02:50:29 +00:00
✨ Add ResolvesEntities concern for name-based ID resolution
- 🤖 Introduced `ResolvesEntities` trait to simplify entity resolution by name or ID across MCP tools. - 📚 Updated tools (Meetups, Cities, Venues, Courses, Lecturers) to use the concern for resolving related entities (e.g., courses, venues, lecturers). - 🎯 Enhanced tool descriptions and schemas for better name-based parameter handling with fallback support for IDs. - ✅ Added dedicated feature tests for name resolution logic, partial matches, and error handling scenarios.
This commit is contained in:
@@ -42,12 +42,28 @@ use Laravel\Mcp\Server\Tool;
|
|||||||
#[Version('1.0.0')]
|
#[Version('1.0.0')]
|
||||||
#[Instructions(<<<'TXT'
|
#[Instructions(<<<'TXT'
|
||||||
Dieser Server spiegelt die authentifizierte Einundzwanzig-API. Jeder Aufruf läuft im Kontext
|
Dieser Server spiegelt die authentifizierte Einundzwanzig-API. Jeder Aufruf läuft im Kontext
|
||||||
des per Sanctum-Token angemeldeten Nutzers; beim Anlegen wird der Ersteller (created_by)
|
des angemeldeten Nutzers; beim Anlegen wird der Ersteller (created_by) automatisch gesetzt.
|
||||||
automatisch auf diesen Nutzer gesetzt. Schreib- und Eigentums-Operationen (update, my-*) sind
|
Schreib- und Eigentums-Operationen (update, show-my-*) sind nur für den Ersteller oder einen
|
||||||
nur für den Ersteller oder einen Super-Admin erlaubt.
|
Super-Admin erlaubt.
|
||||||
|
|
||||||
Fremdschlüssel (city_id, venue_id, lecturer_id, course_id) zuerst über die search-* Tools
|
WICHTIG – niemals nach numerischen IDs fragen: Nutzer kennen keine internen IDs. Referenziere
|
||||||
auflösen, bevor ein Datensatz angelegt oder aktualisiert wird.
|
Entitäten immer über ihren NAMEN:
|
||||||
|
- Eigene Datensätze ändern/anzeigen: zuerst das passende list-my-* Tool aufrufen
|
||||||
|
(list-my-meetups, list-my-cities, list-my-venues, list-my-lecturers, list-my-course-events),
|
||||||
|
dem Nutzer die Namen als Auswahlliste präsentieren und ihn wählen lassen. Dann das update-/
|
||||||
|
show-my-* Tool mit dem gewählten Namen aufrufen (Parameter z. B. "meetup", "city", "venue",
|
||||||
|
"lecturer", "course").
|
||||||
|
- Fremdschlüssel beim Anlegen (Stadt, Land, Referent, Kurs, Veranstaltungsort): den Namen
|
||||||
|
übergeben (Parameter z. B. "city", "country", "lecturer", "course", "venue"); bei Unsicherheit
|
||||||
|
vorher mit search-cities / search-venues / search-lecturers / search-courses / list-countries
|
||||||
|
den genauen Namen ermitteln.
|
||||||
|
Termine/Events (Meetup-Termine, Kurs-Events) haben keinen Namen. Hier zuerst list-my-meetup-
|
||||||
|
events bzw. list-my-course-events aufrufen, dem Nutzer die Einträge zur Auswahl anbieten und
|
||||||
|
die ID des gewählten Eintrags übergeben – ebenfalls ohne den Nutzer nach der ID zu fragen.
|
||||||
|
|
||||||
|
Die Tools lösen Namen serverseitig auf. Bei Mehrdeutigkeit oder fehlendem Treffer liefern sie
|
||||||
|
eine Liste der passenden Einträge zurück – diese dem Nutzer zur Auswahl anbieten. Die *_id-
|
||||||
|
Parameter sind nur ein optionaler Fallback, falls die ID bereits bekannt ist.
|
||||||
TXT)]
|
TXT)]
|
||||||
class EinundzwanzigServer extends Server
|
class EinundzwanzigServer extends Server
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ namespace App\Mcp\Tools\City;
|
|||||||
|
|
||||||
use App\Http\Requests\Api\StoreCityRequest;
|
use App\Http\Requests\Api\StoreCityRequest;
|
||||||
use App\Http\Resources\CityResource;
|
use App\Http\Resources\CityResource;
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
use App\Models\City;
|
use App\Models\City;
|
||||||
|
use App\Models\Country;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
@@ -13,9 +15,11 @@ use Laravel\Mcp\Response;
|
|||||||
use Laravel\Mcp\Server\Attributes\Description;
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
use Laravel\Mcp\Server\Tool;
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
#[Description('Legt eine neue Stadt für den authentifizierten Nutzer an. Der Ersteller (created_by) wird automatisch gesetzt.')]
|
#[Description('Legt eine neue Stadt für den authentifizierten Nutzer an. Das Land wird über seinen Namen angegeben; der Ersteller (created_by) wird automatisch gesetzt.')]
|
||||||
class CreateCityTool extends Tool
|
class CreateCityTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -24,6 +28,10 @@ class CreateCityTool extends Tool
|
|||||||
return Response::error('Nicht berechtigt, eine Stadt anzulegen.');
|
return Response::error('Nicht berechtigt, eine Stadt anzulegen.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($error = $this->mergeForeignKey($request, 'country', 'country_id', Country::query(), 'Land')) {
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
$storeRequest = new StoreCityRequest;
|
$storeRequest = new StoreCityRequest;
|
||||||
|
|
||||||
$validated = $request->validate(
|
$validated = $request->validate(
|
||||||
@@ -42,7 +50,8 @@ class CreateCityTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'country_id' => $schema->integer()->description('ID des zugehörigen Landes.')->required(),
|
'country' => $schema->string()->description('Name des zugehörigen Landes (z. B. "Deutschland"). Wird automatisch aufgelöst – bei Bedarf per list-countries den genauen Namen ermitteln.'),
|
||||||
|
'country_id' => $schema->integer()->description('Optional: ID des Landes, falls bereits bekannt (Alternative zu "country").'),
|
||||||
'name' => $schema->string()->description('Name der Stadt.')->required(),
|
'name' => $schema->string()->description('Name der Stadt.')->required(),
|
||||||
'longitude' => $schema->number()->description('Längengrad der Stadt.')->required(),
|
'longitude' => $schema->number()->description('Längengrad der Stadt.')->required(),
|
||||||
'latitude' => $schema->number()->description('Breitengrad der Stadt.')->required(),
|
'latitude' => $schema->number()->description('Breitengrad der Stadt.')->required(),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Mcp\Tools\City;
|
namespace App\Mcp\Tools\City;
|
||||||
|
|
||||||
use App\Http\Resources\CityResource;
|
use App\Http\Resources\CityResource;
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
use App\Models\City;
|
use App\Models\City;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
@@ -14,15 +15,17 @@ use Laravel\Mcp\Server\Tool;
|
|||||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||||
|
|
||||||
#[IsReadOnly]
|
#[IsReadOnly]
|
||||||
#[Description('Zeigt eine einzelne, vom authentifizierten Nutzer erstellte Stadt.')]
|
#[Description('Zeigt eine deiner Städte (per Name angegeben).')]
|
||||||
class ShowMyCityTool extends Tool
|
class ShowMyCityTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$city = City::find($request->get('id'));
|
$city = $this->resolveOwnedByName($request, City::class, 'Städte', 'city');
|
||||||
|
|
||||||
if (! $city) {
|
if ($city instanceof Response) {
|
||||||
return Response::error('Stadt nicht gefunden.');
|
return $city;
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -40,7 +43,8 @@ class ShowMyCityTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $schema->integer()->description('ID der Stadt.')->required(),
|
'city' => $schema->string()->description('Name der Stadt (aus deinen Städten, siehe list-my-cities).'),
|
||||||
|
'id' => $schema->integer()->description('Optional: ID der Stadt, falls bereits bekannt (Alternative zu "city").'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ namespace App\Mcp\Tools\City;
|
|||||||
|
|
||||||
use App\Http\Requests\Api\UpdateCityRequest;
|
use App\Http\Requests\Api\UpdateCityRequest;
|
||||||
use App\Http\Resources\CityResource;
|
use App\Http\Resources\CityResource;
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
use App\Models\City;
|
use App\Models\City;
|
||||||
|
use App\Models\Country;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
@@ -13,15 +15,17 @@ use Laravel\Mcp\Response;
|
|||||||
use Laravel\Mcp\Server\Attributes\Description;
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
use Laravel\Mcp\Server\Tool;
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
#[Description('Aktualisiert eine bestehende Stadt. Nur der Ersteller oder ein Super-Admin darf sie ändern.')]
|
#[Description('Aktualisiert eine deiner Städte (per Name angegeben). Nur der Ersteller oder ein Super-Admin darf sie ändern.')]
|
||||||
class UpdateCityTool extends Tool
|
class UpdateCityTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$city = City::find($request->get('id'));
|
$city = $this->resolveOwnedByName($request, City::class, 'Städte', 'city');
|
||||||
|
|
||||||
if (! $city) {
|
if ($city instanceof Response) {
|
||||||
return Response::error('Stadt nicht gefunden.');
|
return $city;
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -30,6 +34,10 @@ class UpdateCityTool extends Tool
|
|||||||
return Response::error('Nur der Ersteller oder ein Super-Admin darf diese Stadt ändern.');
|
return Response::error('Nur der Ersteller oder ein Super-Admin darf diese Stadt ändern.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($error = $this->mergeForeignKey($request, 'country', 'country_id', Country::query(), 'Land', false)) {
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validate((new UpdateCityRequest)->rules());
|
$validated = $request->validate((new UpdateCityRequest)->rules());
|
||||||
|
|
||||||
$city->update($validated);
|
$city->update($validated);
|
||||||
@@ -43,9 +51,11 @@ class UpdateCityTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $schema->integer()->description('ID der zu aktualisierenden Stadt.')->required(),
|
'city' => $schema->string()->description('Name der zu ändernden Stadt (aus deinen Städten, siehe list-my-cities).'),
|
||||||
'country_id' => $schema->integer()->description('ID des zugehörigen Landes.'),
|
'id' => $schema->integer()->description('Optional: ID der Stadt, falls bereits bekannt (Alternative zu "city").'),
|
||||||
'name' => $schema->string()->description('Name der Stadt.'),
|
'country' => $schema->string()->description('Name des zugehörigen Landes (wird automatisch aufgelöst).'),
|
||||||
|
'country_id' => $schema->integer()->description('Optional: ID des Landes (Alternative zu "country").'),
|
||||||
|
'name' => $schema->string()->description('Neuer Name der Stadt.'),
|
||||||
'longitude' => $schema->number()->description('Längengrad der Stadt.'),
|
'longitude' => $schema->number()->description('Längengrad der Stadt.'),
|
||||||
'latitude' => $schema->number()->description('Breitengrad der Stadt.'),
|
'latitude' => $schema->number()->description('Breitengrad der Stadt.'),
|
||||||
'population' => $schema->integer()->description('Einwohnerzahl der Stadt.'),
|
'population' => $schema->integer()->description('Einwohnerzahl der Stadt.'),
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Concerns;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löst Entitäten über ihren Namen (oder optional ihre ID) auf, damit Nutzer keine
|
||||||
|
* internen IDs kennen müssen. Bei Mehrdeutigkeit oder fehlendem Treffer wird eine
|
||||||
|
* Auswahlliste der passenden Einträge als Fehlertext zurückgegeben.
|
||||||
|
*/
|
||||||
|
trait ResolvesEntities
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Löst einen vom authentifizierten Nutzer erstellten Datensatz auf.
|
||||||
|
*
|
||||||
|
* Die ID-Variante sucht über alle Datensätze (die Ownership-Prüfung übernimmt die
|
||||||
|
* aufrufende Gate-Policy); die Namens-Variante ist auf die eigenen Datensätze
|
||||||
|
* beschränkt und dient damit zugleich als Auswahlliste.
|
||||||
|
*
|
||||||
|
* @param class-string<Model> $modelClass
|
||||||
|
*/
|
||||||
|
protected function resolveOwnedByName(Request $request, string $modelClass, string $label, string $nameParam, string $column = 'name'): Model|Response
|
||||||
|
{
|
||||||
|
$id = $request->get('id');
|
||||||
|
|
||||||
|
if ($this->present($id)) {
|
||||||
|
$found = $modelClass::query()->whereKey($id)->first();
|
||||||
|
|
||||||
|
if ($found !== null) {
|
||||||
|
return $found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$owned = $modelClass::query()->where('created_by', $request->user()?->getAuthIdentifier());
|
||||||
|
$name = $request->get($nameParam);
|
||||||
|
|
||||||
|
if ($this->present($name)) {
|
||||||
|
$matches = $this->matchByName($owned, (string) $name, $column);
|
||||||
|
|
||||||
|
if ($matches->count() === 1) {
|
||||||
|
return $matches->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($matches->count() > 1) {
|
||||||
|
return Response::error("Mehrere {$label} passen zu \"{$name}\": ".$matches->pluck($column)->join('; ').'. Bitte den genauen Namen angeben.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->optionsError($owned, $label, $column);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löst einen Fremdschlüssel über den Namen auf und schreibt die ID in den Request,
|
||||||
|
* damit die nachgelagerte Validierung sie sieht. Gibt null zurück, wenn nichts zu tun
|
||||||
|
* ist (ID bereits gesetzt, oder optionaler FK ohne Namen), sonst eine Fehler-Response.
|
||||||
|
*/
|
||||||
|
protected function mergeForeignKey(Request $request, string $nameParam, string $idKey, Builder $query, string $label, bool $required = true): ?Response
|
||||||
|
{
|
||||||
|
if ($this->present($request->get($idKey))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->present($request->get($nameParam))) {
|
||||||
|
return $required ? Response::error("Bitte einen Namen für {$label} angeben.") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = $this->resolveGlobalByName($query, $request->get($nameParam), $label);
|
||||||
|
|
||||||
|
if ($model instanceof Response) {
|
||||||
|
return $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->merge([$idKey => $model->id]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löst einen global sichtbaren Datensatz (z. B. Stadt, Land, Referent, Kurs, Ort)
|
||||||
|
* über seinen Namen auf.
|
||||||
|
*/
|
||||||
|
protected function resolveGlobalByName(Builder $query, ?string $name, string $label, string $column = 'name'): Model|Response
|
||||||
|
{
|
||||||
|
if (! $this->present($name)) {
|
||||||
|
return Response::error("Bitte einen Namen für {$label} angeben.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$matches = $this->matchByName($query, (string) $name, $column);
|
||||||
|
|
||||||
|
if ($matches->count() === 1) {
|
||||||
|
return $matches->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($matches->isEmpty()) {
|
||||||
|
return Response::error("{$label} \"{$name}\" wurde nicht gefunden. Nutze das passende search-Tool, um den genauen Namen zu ermitteln.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::error("Mehrere {$label} passen zu \"{$name}\": ".$matches->pluck($column)->take(15)->join('; ').'. Bitte präziser angeben.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case-insensitive Treffer in einer einzigen Abfrage: Teilstring-Suche, exakte
|
||||||
|
* Treffer nach vorne sortiert (und dadurch nie vom Limit abgeschnitten). Existiert
|
||||||
|
* mindestens ein exakter Treffer, gewinnt dieser; sonst zählen die Teiltreffer.
|
||||||
|
* DB-portabel über LOWER().
|
||||||
|
*
|
||||||
|
* @return Collection<int, Model>
|
||||||
|
*/
|
||||||
|
private function matchByName(Builder $query, string $name, string $column): Collection
|
||||||
|
{
|
||||||
|
$needle = mb_strtolower(trim($name));
|
||||||
|
|
||||||
|
$matches = (clone $query)
|
||||||
|
->whereRaw('LOWER('.$column.') LIKE ?', ['%'.$needle.'%'])
|
||||||
|
->orderByRaw('CASE WHEN LOWER('.$column.') = ? THEN 0 ELSE 1 END', [$needle])
|
||||||
|
->limit(25)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$exact = $matches->filter(
|
||||||
|
fn (Model $model): bool => mb_strtolower((string) $model->getAttribute($column)) === $needle
|
||||||
|
)->values();
|
||||||
|
|
||||||
|
return $exact->isNotEmpty() ? $exact : $matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function optionsError(Builder $owned, string $label, string $column): Response
|
||||||
|
{
|
||||||
|
$names = (clone $owned)->orderBy($column)->limit(50)->pluck($column);
|
||||||
|
|
||||||
|
if ($names->isEmpty()) {
|
||||||
|
return Response::error("Du hast noch keine {$label} angelegt.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::error("{$label} nicht gefunden. Deine {$label}: ".$names->join('; ').'.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function present(mixed $value): bool
|
||||||
|
{
|
||||||
|
return $value !== null && $value !== '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Mcp\Tools\Course;
|
namespace App\Mcp\Tools\Course;
|
||||||
|
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
use App\Models\Course;
|
use App\Models\Course;
|
||||||
|
use App\Models\Lecturer;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
@@ -11,9 +13,11 @@ use Laravel\Mcp\Response;
|
|||||||
use Laravel\Mcp\Server\Attributes\Description;
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
use Laravel\Mcp\Server\Tool;
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
#[Description('Legt einen neuen Kurs für den authentifizierten Referenten an. Der Ersteller (created_by) wird automatisch gesetzt.')]
|
#[Description('Legt einen neuen Kurs für den authentifizierten Referenten an. Der Referent wird über seinen Namen angegeben; der Ersteller (created_by) wird automatisch gesetzt.')]
|
||||||
class CreateCourseTool extends Tool
|
class CreateCourseTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -22,6 +26,10 @@ class CreateCourseTool extends Tool
|
|||||||
return Response::error('Nur Referenten (is_lecturer) dürfen Kurse anlegen.');
|
return Response::error('Nur Referenten (is_lecturer) dürfen Kurse anlegen.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($error = $this->mergeForeignKey($request, 'lecturer', 'lecturer_id', Lecturer::query(), 'Referenten')) {
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'lecturer_id' => ['required', 'exists:lecturers,id'],
|
'lecturer_id' => ['required', 'exists:lecturers,id'],
|
||||||
@@ -40,7 +48,8 @@ class CreateCourseTool extends Tool
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => $schema->string()->description('Name des Kurses.')->required(),
|
'name' => $schema->string()->description('Name des Kurses.')->required(),
|
||||||
'lecturer_id' => $schema->integer()->description('ID des zugehörigen Referenten (vorher per search-lecturers auflösen).')->required(),
|
'lecturer' => $schema->string()->description('Name des zugehörigen Referenten. Wird automatisch aufgelöst – bei Bedarf per search-lecturers den genauen Namen ermitteln.'),
|
||||||
|
'lecturer_id' => $schema->integer()->description('Optional: ID des Referenten, falls bereits bekannt (Alternative zu "lecturer").'),
|
||||||
'description' => $schema->string()->description('Beschreibung des Kurses.'),
|
'description' => $schema->string()->description('Beschreibung des Kurses.'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Mcp\Tools\Course;
|
namespace App\Mcp\Tools\Course;
|
||||||
|
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
use App\Models\Course;
|
use App\Models\Course;
|
||||||
|
use App\Models\Lecturer;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
@@ -11,15 +13,17 @@ use Laravel\Mcp\Response;
|
|||||||
use Laravel\Mcp\Server\Attributes\Description;
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
use Laravel\Mcp\Server\Tool;
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
#[Description('Aktualisiert einen bestehenden Kurs. Nur der Ersteller oder ein Super-Admin darf ihn ändern.')]
|
#[Description('Aktualisiert einen deiner Kurse (per Name angegeben). Nur der Ersteller oder ein Super-Admin darf ihn ändern.')]
|
||||||
class UpdateCourseTool extends Tool
|
class UpdateCourseTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$course = Course::find($request->get('id'));
|
$course = $this->resolveOwnedByName($request, Course::class, 'Kurse', 'course');
|
||||||
|
|
||||||
if (! $course) {
|
if ($course instanceof Response) {
|
||||||
return Response::error('Kurs nicht gefunden.');
|
return $course;
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -28,6 +32,10 @@ class UpdateCourseTool extends Tool
|
|||||||
return Response::error('Nur der Ersteller des Kurses oder ein Super-Admin darf ihn ändern.');
|
return Response::error('Nur der Ersteller des Kurses oder ein Super-Admin darf ihn ändern.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($error = $this->mergeForeignKey($request, 'lecturer', 'lecturer_id', Lecturer::query(), 'Referenten', false)) {
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => ['sometimes', 'required', 'string', 'max:255'],
|
'name' => ['sometimes', 'required', 'string', 'max:255'],
|
||||||
'lecturer_id' => ['sometimes', 'required', 'exists:lecturers,id'],
|
'lecturer_id' => ['sometimes', 'required', 'exists:lecturers,id'],
|
||||||
@@ -45,9 +53,11 @@ class UpdateCourseTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $schema->integer()->description('ID des zu aktualisierenden Kurses.')->required(),
|
'course' => $schema->string()->description('Name des zu ändernden Kurses (aus deinen Kursen, siehe list-my-course-events bzw. search-courses).'),
|
||||||
'name' => $schema->string()->description('Name des Kurses.'),
|
'id' => $schema->integer()->description('Optional: ID des Kurses, falls bereits bekannt (Alternative zu "course").'),
|
||||||
'lecturer_id' => $schema->integer()->description('ID des zugehörigen Referenten.'),
|
'name' => $schema->string()->description('Neuer Name des Kurses.'),
|
||||||
|
'lecturer' => $schema->string()->description('Name des zugehörigen Referenten (wird automatisch aufgelöst).'),
|
||||||
|
'lecturer_id' => $schema->integer()->description('Optional: ID des Referenten (Alternative zu "lecturer").'),
|
||||||
'description' => $schema->string()->description('Beschreibung des Kurses.'),
|
'description' => $schema->string()->description('Beschreibung des Kurses.'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Mcp\Tools\CourseEvent;
|
namespace App\Mcp\Tools\CourseEvent;
|
||||||
|
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
|
use App\Models\Course;
|
||||||
use App\Models\CourseEvent;
|
use App\Models\CourseEvent;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\Venue;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
use Laravel\Mcp\Request;
|
use Laravel\Mcp\Request;
|
||||||
@@ -11,9 +14,11 @@ use Laravel\Mcp\Response;
|
|||||||
use Laravel\Mcp\Server\Attributes\Description;
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
use Laravel\Mcp\Server\Tool;
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
#[Description('Legt ein neues Kurs-Event für den authentifizierten Referenten an. Der Ersteller (created_by) wird automatisch gesetzt.')]
|
#[Description('Legt ein neues Kurs-Event für den authentifizierten Referenten an. Kurs und Veranstaltungsort werden über ihre Namen angegeben; der Ersteller (created_by) wird automatisch gesetzt.')]
|
||||||
class CreateCourseEventTool extends Tool
|
class CreateCourseEventTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -22,6 +27,20 @@ class CreateCourseEventTool extends Tool
|
|||||||
return Response::error('Nur Referenten (is_lecturer) dürfen Kurs-Events anlegen.');
|
return Response::error('Nur Referenten (is_lecturer) dürfen Kurs-Events anlegen.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! $this->present($request->get('course_id'))) {
|
||||||
|
$course = $this->resolveOwnedByName($request, Course::class, 'Kurse', 'course');
|
||||||
|
|
||||||
|
if ($course instanceof Response) {
|
||||||
|
return $course;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->merge(['course_id' => $course->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error = $this->mergeForeignKey($request, 'venue', 'venue_id', Venue::query(), 'Veranstaltungsorte')) {
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'course_id' => ['required', 'integer', 'exists:courses,id'],
|
'course_id' => ['required', 'integer', 'exists:courses,id'],
|
||||||
'venue_id' => ['required', 'integer', 'exists:venues,id'],
|
'venue_id' => ['required', 'integer', 'exists:venues,id'],
|
||||||
@@ -41,8 +60,10 @@ class CreateCourseEventTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'course_id' => $schema->integer()->description('ID des zugehörigen Kurses (vorher per search-courses auflösen).')->required(),
|
'course' => $schema->string()->description('Name deines Kurses, zu dem das Event gehört. Wird automatisch aufgelöst – sonst zuerst search-courses aufrufen.'),
|
||||||
'venue_id' => $schema->integer()->description('ID des Veranstaltungsorts (vorher per search-venues auflösen).')->required(),
|
'course_id' => $schema->integer()->description('Optional: ID des Kurses, falls bereits bekannt (Alternative zu "course").'),
|
||||||
|
'venue' => $schema->string()->description('Name des Veranstaltungsorts. Wird automatisch aufgelöst – bei Bedarf per search-venues den genauen Namen ermitteln.'),
|
||||||
|
'venue_id' => $schema->integer()->description('Optional: ID des Veranstaltungsorts, falls bereits bekannt (Alternative zu "venue").'),
|
||||||
'from' => $schema->string()->description('Startzeitpunkt (Datum/Uhrzeit).')->required(),
|
'from' => $schema->string()->description('Startzeitpunkt (Datum/Uhrzeit).')->required(),
|
||||||
'to' => $schema->string()->description('Endzeitpunkt (Datum/Uhrzeit), gleich oder nach dem Start.')->required(),
|
'to' => $schema->string()->description('Endzeitpunkt (Datum/Uhrzeit), gleich oder nach dem Start.')->required(),
|
||||||
'link' => $schema->string()->description('URL mit weiteren Informationen zum Kurs-Event.')->required(),
|
'link' => $schema->string()->description('URL mit weiteren Informationen zum Kurs-Event.')->required(),
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Mcp\Tools\CourseEvent;
|
namespace App\Mcp\Tools\CourseEvent;
|
||||||
|
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
|
use App\Models\Course;
|
||||||
use App\Models\CourseEvent;
|
use App\Models\CourseEvent;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\Venue;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
use Laravel\Mcp\Request;
|
use Laravel\Mcp\Request;
|
||||||
@@ -14,6 +17,8 @@ use Laravel\Mcp\Server\Tool;
|
|||||||
#[Description('Aktualisiert ein bestehendes Kurs-Event. Nur der Ersteller oder ein Super-Admin darf es ändern.')]
|
#[Description('Aktualisiert ein bestehendes Kurs-Event. Nur der Ersteller oder ein Super-Admin darf es ändern.')]
|
||||||
class UpdateCourseEventTool extends Tool
|
class UpdateCourseEventTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$courseEvent = CourseEvent::find($request->get('id'));
|
$courseEvent = CourseEvent::find($request->get('id'));
|
||||||
@@ -28,6 +33,14 @@ class UpdateCourseEventTool extends Tool
|
|||||||
return Response::error('Nur der Ersteller des Kurs-Events oder ein Super-Admin darf es ändern.');
|
return Response::error('Nur der Ersteller des Kurs-Events oder ein Super-Admin darf es ändern.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($error = $this->mergeForeignKey($request, 'course', 'course_id', Course::query()->where('created_by', $user->getAuthIdentifier()), 'Kurse', false)) {
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error = $this->mergeForeignKey($request, 'venue', 'venue_id', Venue::query(), 'Veranstaltungsorte', false)) {
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'course_id' => ['sometimes', 'required', 'integer', 'exists:courses,id'],
|
'course_id' => ['sometimes', 'required', 'integer', 'exists:courses,id'],
|
||||||
'venue_id' => ['sometimes', 'required', 'integer', 'exists:venues,id'],
|
'venue_id' => ['sometimes', 'required', 'integer', 'exists:venues,id'],
|
||||||
@@ -47,9 +60,11 @@ class UpdateCourseEventTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $schema->integer()->description('ID des zu aktualisierenden Kurs-Events.')->required(),
|
'id' => $schema->integer()->description('ID des zu aktualisierenden Kurs-Events (über list-my-course-events ermitteln; nicht den Nutzer danach fragen).')->required(),
|
||||||
'course_id' => $schema->integer()->description('ID des zugehörigen Kurses.'),
|
'course' => $schema->string()->description('Name des zugehörigen Kurses, falls geändert werden soll (wird automatisch aufgelöst).'),
|
||||||
'venue_id' => $schema->integer()->description('ID des Veranstaltungsorts.'),
|
'course_id' => $schema->integer()->description('Optional: ID des Kurses (Alternative zu "course").'),
|
||||||
|
'venue' => $schema->string()->description('Name des Veranstaltungsorts, falls geändert werden soll (wird automatisch aufgelöst).'),
|
||||||
|
'venue_id' => $schema->integer()->description('Optional: ID des Veranstaltungsorts (Alternative zu "venue").'),
|
||||||
'from' => $schema->string()->description('Startzeitpunkt (Datum/Uhrzeit).'),
|
'from' => $schema->string()->description('Startzeitpunkt (Datum/Uhrzeit).'),
|
||||||
'to' => $schema->string()->description('Endzeitpunkt (Datum/Uhrzeit), gleich oder nach dem Start.'),
|
'to' => $schema->string()->description('Endzeitpunkt (Datum/Uhrzeit), gleich oder nach dem Start.'),
|
||||||
'link' => $schema->string()->description('URL mit weiteren Informationen zum Kurs-Event.'),
|
'link' => $schema->string()->description('URL mit weiteren Informationen zum Kurs-Event.'),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Mcp\Tools\Lecturer;
|
namespace App\Mcp\Tools\Lecturer;
|
||||||
|
|
||||||
use App\Http\Resources\LecturerResource;
|
use App\Http\Resources\LecturerResource;
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
use App\Models\Lecturer;
|
use App\Models\Lecturer;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
@@ -14,15 +15,17 @@ use Laravel\Mcp\Server\Tool;
|
|||||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||||
|
|
||||||
#[IsReadOnly]
|
#[IsReadOnly]
|
||||||
#[Description('Zeigt einen einzelnen, vom authentifizierten Nutzer erstellten Referenten.')]
|
#[Description('Zeigt einen deiner Referenten (per Name angegeben).')]
|
||||||
class ShowMyLecturerTool extends Tool
|
class ShowMyLecturerTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$lecturer = Lecturer::find($request->get('id'));
|
$lecturer = $this->resolveOwnedByName($request, Lecturer::class, 'Referenten', 'lecturer');
|
||||||
|
|
||||||
if (! $lecturer) {
|
if ($lecturer instanceof Response) {
|
||||||
return Response::error('Referent nicht gefunden.');
|
return $lecturer;
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -40,7 +43,8 @@ class ShowMyLecturerTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $schema->integer()->description('ID des Referenten.')->required(),
|
'lecturer' => $schema->string()->description('Name des Referenten (aus deinen Referenten, siehe list-my-lecturers).'),
|
||||||
|
'id' => $schema->integer()->description('Optional: ID des Referenten, falls bereits bekannt (Alternative zu "lecturer").'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Mcp\Tools\Lecturer;
|
|||||||
|
|
||||||
use App\Http\Requests\Api\UpdateLecturerRequest;
|
use App\Http\Requests\Api\UpdateLecturerRequest;
|
||||||
use App\Http\Resources\LecturerResource;
|
use App\Http\Resources\LecturerResource;
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
use App\Models\Lecturer;
|
use App\Models\Lecturer;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
@@ -13,15 +14,17 @@ use Laravel\Mcp\Response;
|
|||||||
use Laravel\Mcp\Server\Attributes\Description;
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
use Laravel\Mcp\Server\Tool;
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
#[Description('Aktualisiert einen bestehenden Referenten. Nur der Ersteller oder ein Super-Admin darf ihn ändern.')]
|
#[Description('Aktualisiert einen deiner Referenten (per Name angegeben). Nur der Ersteller oder ein Super-Admin darf ihn ändern.')]
|
||||||
class UpdateLecturerTool extends Tool
|
class UpdateLecturerTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$lecturer = Lecturer::find($request->get('id'));
|
$lecturer = $this->resolveOwnedByName($request, Lecturer::class, 'Referenten', 'lecturer');
|
||||||
|
|
||||||
if (! $lecturer) {
|
if ($lecturer instanceof Response) {
|
||||||
return Response::error('Referent nicht gefunden.');
|
return $lecturer;
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -43,8 +46,9 @@ class UpdateLecturerTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $schema->integer()->description('ID des zu aktualisierenden Referenten.')->required(),
|
'lecturer' => $schema->string()->description('Name des zu ändernden Referenten (aus deinen Referenten, siehe list-my-lecturers).'),
|
||||||
'name' => $schema->string()->description('Name des Referenten.'),
|
'id' => $schema->integer()->description('Optional: ID des Referenten, falls bereits bekannt (Alternative zu "lecturer").'),
|
||||||
|
'name' => $schema->string()->description('Neuer Name des Referenten.'),
|
||||||
'subtitle' => $schema->string()->description('Untertitel.'),
|
'subtitle' => $schema->string()->description('Untertitel.'),
|
||||||
'intro' => $schema->string()->description('Einleitungstext.'),
|
'intro' => $schema->string()->description('Einleitungstext.'),
|
||||||
'description' => $schema->string()->description('Beschreibung.'),
|
'description' => $schema->string()->description('Beschreibung.'),
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Mcp\Tools\Meetup;
|
|||||||
|
|
||||||
use App\Http\Requests\Api\StoreMeetupRequest;
|
use App\Http\Requests\Api\StoreMeetupRequest;
|
||||||
use App\Http\Resources\MeetupResource;
|
use App\Http\Resources\MeetupResource;
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
|
use App\Models\City;
|
||||||
use App\Models\Meetup;
|
use App\Models\Meetup;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
@@ -13,9 +15,11 @@ use Laravel\Mcp\Response;
|
|||||||
use Laravel\Mcp\Server\Attributes\Description;
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
use Laravel\Mcp\Server\Tool;
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
#[Description('Legt ein neues Meetup für den authentifizierten Nutzer an. Der Ersteller (created_by) wird automatisch gesetzt.')]
|
#[Description('Legt ein neues Meetup für den authentifizierten Nutzer an. Die Stadt wird über ihren Namen angegeben; der Ersteller (created_by) wird automatisch gesetzt.')]
|
||||||
class CreateMeetupTool extends Tool
|
class CreateMeetupTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -24,6 +28,10 @@ class CreateMeetupTool extends Tool
|
|||||||
return Response::error('Nicht berechtigt, ein Meetup anzulegen.');
|
return Response::error('Nicht berechtigt, ein Meetup anzulegen.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($error = $this->mergeForeignKey($request, 'city', 'city_id', City::query(), 'Stadt')) {
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
$storeRequest = new StoreMeetupRequest;
|
$storeRequest = new StoreMeetupRequest;
|
||||||
|
|
||||||
$validated = $request->validate(
|
$validated = $request->validate(
|
||||||
@@ -43,7 +51,8 @@ class CreateMeetupTool extends Tool
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => $schema->string()->description('Name des Meetups.')->required(),
|
'name' => $schema->string()->description('Name des Meetups.')->required(),
|
||||||
'city_id' => $schema->integer()->description('ID der zugehörigen Stadt (vorher per search-cities auflösen).')->required(),
|
'city' => $schema->string()->description('Name der zugehörigen Stadt (z. B. "Ansbach"). Wird automatisch aufgelöst – bei Bedarf per search-cities den genauen Namen ermitteln.'),
|
||||||
|
'city_id' => $schema->integer()->description('Optional: ID der Stadt, falls bereits bekannt (Alternative zu "city").'),
|
||||||
'intro' => $schema->string()->description('Einleitungstext.'),
|
'intro' => $schema->string()->description('Einleitungstext.'),
|
||||||
'telegram_link' => $schema->string()->description('Telegram-Gruppen-URL.'),
|
'telegram_link' => $schema->string()->description('Telegram-Gruppen-URL.'),
|
||||||
'webpage' => $schema->string()->description('Webseiten-URL.'),
|
'webpage' => $schema->string()->description('Webseiten-URL.'),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Mcp\Tools\Meetup;
|
namespace App\Mcp\Tools\Meetup;
|
||||||
|
|
||||||
use App\Http\Resources\MeetupResource;
|
use App\Http\Resources\MeetupResource;
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
use App\Models\Meetup;
|
use App\Models\Meetup;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
@@ -14,15 +15,17 @@ use Laravel\Mcp\Server\Tool;
|
|||||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||||
|
|
||||||
#[IsReadOnly]
|
#[IsReadOnly]
|
||||||
#[Description('Zeigt ein einzelnes, vom authentifizierten Nutzer erstelltes Meetup.')]
|
#[Description('Zeigt eines deiner Meetups (per Name angegeben).')]
|
||||||
class ShowMyMeetupTool extends Tool
|
class ShowMyMeetupTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$meetup = Meetup::find($request->get('id'));
|
$meetup = $this->resolveOwnedByName($request, Meetup::class, 'Meetups', 'meetup');
|
||||||
|
|
||||||
if (! $meetup) {
|
if ($meetup instanceof Response) {
|
||||||
return Response::error('Meetup nicht gefunden.');
|
return $meetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -40,7 +43,8 @@ class ShowMyMeetupTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $schema->integer()->description('ID des Meetups.')->required(),
|
'meetup' => $schema->string()->description('Name des Meetups (aus deinen Meetups, siehe list-my-meetups).'),
|
||||||
|
'id' => $schema->integer()->description('Optional: ID des Meetups, falls bereits bekannt (Alternative zu "meetup").'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Mcp\Tools\Meetup;
|
|||||||
|
|
||||||
use App\Http\Requests\Api\UpdateMeetupRequest;
|
use App\Http\Requests\Api\UpdateMeetupRequest;
|
||||||
use App\Http\Resources\MeetupResource;
|
use App\Http\Resources\MeetupResource;
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
|
use App\Models\City;
|
||||||
use App\Models\Meetup;
|
use App\Models\Meetup;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
@@ -13,15 +15,17 @@ use Laravel\Mcp\Response;
|
|||||||
use Laravel\Mcp\Server\Attributes\Description;
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
use Laravel\Mcp\Server\Tool;
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
#[Description('Aktualisiert ein bestehendes Meetup. Nur der Ersteller oder ein Super-Admin darf es ändern.')]
|
#[Description('Aktualisiert eines deiner Meetups (per Name angegeben). Nur der Ersteller oder ein Super-Admin darf es ändern.')]
|
||||||
class UpdateMeetupTool extends Tool
|
class UpdateMeetupTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$meetup = Meetup::find($request->get('id'));
|
$meetup = $this->resolveOwnedByName($request, Meetup::class, 'Meetups', 'meetup');
|
||||||
|
|
||||||
if (! $meetup) {
|
if ($meetup instanceof Response) {
|
||||||
return Response::error('Meetup nicht gefunden.');
|
return $meetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -30,6 +34,10 @@ class UpdateMeetupTool extends Tool
|
|||||||
return Response::error('Nur der Ersteller oder ein Super-Admin darf dieses Meetup ändern.');
|
return Response::error('Nur der Ersteller oder ein Super-Admin darf dieses Meetup ändern.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($error = $this->mergeForeignKey($request, 'city', 'city_id', City::query(), 'Stadt', false)) {
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validate((new UpdateMeetupRequest)->rules());
|
$validated = $request->validate((new UpdateMeetupRequest)->rules());
|
||||||
|
|
||||||
$meetup->update($validated);
|
$meetup->update($validated);
|
||||||
@@ -43,9 +51,11 @@ class UpdateMeetupTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $schema->integer()->description('ID des zu aktualisierenden Meetups.')->required(),
|
'meetup' => $schema->string()->description('Name des zu ändernden Meetups (aus deinen Meetups, siehe list-my-meetups).'),
|
||||||
'name' => $schema->string()->description('Name des Meetups.'),
|
'id' => $schema->integer()->description('Optional: ID des Meetups, falls bereits bekannt (Alternative zu "meetup").'),
|
||||||
'city_id' => $schema->integer()->description('ID der zugehörigen Stadt.'),
|
'name' => $schema->string()->description('Neuer Name des Meetups.'),
|
||||||
|
'city' => $schema->string()->description('Name der zugehörigen Stadt (wird automatisch aufgelöst).'),
|
||||||
|
'city_id' => $schema->integer()->description('Optional: ID der Stadt (Alternative zu "city").'),
|
||||||
'intro' => $schema->string()->description('Einleitungstext.'),
|
'intro' => $schema->string()->description('Einleitungstext.'),
|
||||||
'telegram_link' => $schema->string()->description('Telegram-Gruppen-URL.'),
|
'telegram_link' => $schema->string()->description('Telegram-Gruppen-URL.'),
|
||||||
'webpage' => $schema->string()->description('Webseiten-URL.'),
|
'webpage' => $schema->string()->description('Webseiten-URL.'),
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Mcp\Tools\MeetupEvent;
|
|||||||
|
|
||||||
use App\Http\Requests\Api\StoreMeetupEventRequest;
|
use App\Http\Requests\Api\StoreMeetupEventRequest;
|
||||||
use App\Http\Resources\MeetupEventResource;
|
use App\Http\Resources\MeetupEventResource;
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
|
use App\Models\Meetup;
|
||||||
use App\Models\MeetupEvent;
|
use App\Models\MeetupEvent;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
@@ -13,9 +15,11 @@ use Laravel\Mcp\Response;
|
|||||||
use Laravel\Mcp\Server\Attributes\Description;
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
use Laravel\Mcp\Server\Tool;
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
#[Description('Legt einen neuen Meetup-Termin für den authentifizierten Nutzer an. Der Ersteller (created_by) wird automatisch gesetzt.')]
|
#[Description('Legt einen neuen Meetup-Termin für eines der eigenen Meetups an. Das Meetup wird über seinen Namen angegeben; der Ersteller (created_by) wird automatisch gesetzt.')]
|
||||||
class CreateMeetupEventTool extends Tool
|
class CreateMeetupEventTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -24,6 +28,16 @@ class CreateMeetupEventTool extends Tool
|
|||||||
return Response::error('Nicht berechtigt, einen Meetup-Termin anzulegen.');
|
return Response::error('Nicht berechtigt, einen Meetup-Termin anzulegen.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! $this->present($request->get('meetup_id'))) {
|
||||||
|
$meetup = $this->resolveOwnedByName($request, Meetup::class, 'Meetups', 'meetup');
|
||||||
|
|
||||||
|
if ($meetup instanceof Response) {
|
||||||
|
return $meetup;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->merge(['meetup_id' => $meetup->id]);
|
||||||
|
}
|
||||||
|
|
||||||
$storeRequest = new StoreMeetupEventRequest;
|
$storeRequest = new StoreMeetupEventRequest;
|
||||||
|
|
||||||
$validated = $request->validate(
|
$validated = $request->validate(
|
||||||
@@ -42,7 +56,8 @@ class CreateMeetupEventTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'meetup_id' => $schema->integer()->description('ID des zugehörigen Meetups (vorher per search-meetups auflösen).')->required(),
|
'meetup' => $schema->string()->description('Name deines Meetups, zu dem der Termin gehört (z. B. "Einundzwanzig Ansbach"). Wird automatisch aufgelöst – sonst zuerst list-my-meetups aufrufen und den Nutzer auswählen lassen.'),
|
||||||
|
'meetup_id' => $schema->integer()->description('Optional: ID des Meetups, falls bereits bekannt (Alternative zu "meetup").'),
|
||||||
'start' => $schema->string()->description('Startzeitpunkt als Datum/Uhrzeit (z. B. 2026-08-01 18:00:00).')->required(),
|
'start' => $schema->string()->description('Startzeitpunkt als Datum/Uhrzeit (z. B. 2026-08-01 18:00:00).')->required(),
|
||||||
'location' => $schema->string()->description('Veranstaltungsort.'),
|
'location' => $schema->string()->description('Veranstaltungsort.'),
|
||||||
'description' => $schema->string()->description('Beschreibung des Termins.'),
|
'description' => $schema->string()->description('Beschreibung des Termins.'),
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class ShowMyMeetupEventTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $schema->integer()->description('ID des Meetup-Termins.')->required(),
|
'id' => $schema->integer()->description('ID des Meetup-Termins (über list-my-meetup-events ermitteln; nicht den Nutzer danach fragen).')->required(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Mcp\Tools\MeetupEvent;
|
|||||||
|
|
||||||
use App\Http\Requests\Api\UpdateMeetupEventRequest;
|
use App\Http\Requests\Api\UpdateMeetupEventRequest;
|
||||||
use App\Http\Resources\MeetupEventResource;
|
use App\Http\Resources\MeetupEventResource;
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
|
use App\Models\Meetup;
|
||||||
use App\Models\MeetupEvent;
|
use App\Models\MeetupEvent;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
@@ -16,6 +18,8 @@ use Laravel\Mcp\Server\Tool;
|
|||||||
#[Description('Aktualisiert einen bestehenden Meetup-Termin. Nur der Ersteller oder ein Super-Admin darf ihn ändern.')]
|
#[Description('Aktualisiert einen bestehenden Meetup-Termin. Nur der Ersteller oder ein Super-Admin darf ihn ändern.')]
|
||||||
class UpdateMeetupEventTool extends Tool
|
class UpdateMeetupEventTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$meetupEvent = MeetupEvent::find($request->get('id'));
|
$meetupEvent = MeetupEvent::find($request->get('id'));
|
||||||
@@ -30,6 +34,10 @@ class UpdateMeetupEventTool extends Tool
|
|||||||
return Response::error('Nur der Ersteller oder ein Super-Admin darf diesen Meetup-Termin ändern.');
|
return Response::error('Nur der Ersteller oder ein Super-Admin darf diesen Meetup-Termin ändern.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($error = $this->mergeForeignKey($request, 'meetup', 'meetup_id', Meetup::query()->where('created_by', $user->getAuthIdentifier()), 'Meetups', false)) {
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validate((new UpdateMeetupEventRequest)->rules());
|
$validated = $request->validate((new UpdateMeetupEventRequest)->rules());
|
||||||
|
|
||||||
$meetupEvent->update($validated);
|
$meetupEvent->update($validated);
|
||||||
@@ -43,8 +51,9 @@ class UpdateMeetupEventTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $schema->integer()->description('ID des zu aktualisierenden Meetup-Termins.')->required(),
|
'id' => $schema->integer()->description('ID des zu aktualisierenden Meetup-Termins (über list-my-meetup-events ermitteln; nicht den Nutzer danach fragen).')->required(),
|
||||||
'meetup_id' => $schema->integer()->description('ID des zugehörigen Meetups.'),
|
'meetup' => $schema->string()->description('Name des zugehörigen Meetups, falls geändert werden soll (wird automatisch aufgelöst).'),
|
||||||
|
'meetup_id' => $schema->integer()->description('Optional: ID des Meetups (Alternative zu "meetup").'),
|
||||||
'start' => $schema->string()->description('Startzeitpunkt als Datum/Uhrzeit (z. B. 2026-08-01 18:00:00).'),
|
'start' => $schema->string()->description('Startzeitpunkt als Datum/Uhrzeit (z. B. 2026-08-01 18:00:00).'),
|
||||||
'location' => $schema->string()->description('Veranstaltungsort.'),
|
'location' => $schema->string()->description('Veranstaltungsort.'),
|
||||||
'description' => $schema->string()->description('Beschreibung des Termins.'),
|
'description' => $schema->string()->description('Beschreibung des Termins.'),
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Mcp\Tools\Venue;
|
|||||||
|
|
||||||
use App\Http\Requests\Api\StoreVenueRequest;
|
use App\Http\Requests\Api\StoreVenueRequest;
|
||||||
use App\Http\Resources\VenueResource;
|
use App\Http\Resources\VenueResource;
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
|
use App\Models\City;
|
||||||
use App\Models\Venue;
|
use App\Models\Venue;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
@@ -13,9 +15,11 @@ use Laravel\Mcp\Response;
|
|||||||
use Laravel\Mcp\Server\Attributes\Description;
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
use Laravel\Mcp\Server\Tool;
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
#[Description('Legt einen neuen Veranstaltungsort (Venue) für den authentifizierten Nutzer an. Der Ersteller (created_by) wird automatisch gesetzt.')]
|
#[Description('Legt einen neuen Veranstaltungsort (Venue) für den authentifizierten Nutzer an. Die Stadt wird über ihren Namen angegeben; der Ersteller (created_by) wird automatisch gesetzt.')]
|
||||||
class CreateVenueTool extends Tool
|
class CreateVenueTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -24,6 +28,10 @@ class CreateVenueTool extends Tool
|
|||||||
return Response::error('Nicht berechtigt, einen Veranstaltungsort anzulegen.');
|
return Response::error('Nicht berechtigt, einen Veranstaltungsort anzulegen.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($error = $this->mergeForeignKey($request, 'city', 'city_id', City::query(), 'Stadt')) {
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
$storeRequest = new StoreVenueRequest;
|
$storeRequest = new StoreVenueRequest;
|
||||||
|
|
||||||
$validated = $request->validate(
|
$validated = $request->validate(
|
||||||
@@ -42,7 +50,8 @@ class CreateVenueTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'city_id' => $schema->integer()->description('ID der zugehörigen Stadt (vorher per search-cities auflösen).')->required(),
|
'city' => $schema->string()->description('Name der zugehörigen Stadt (z. B. "Ansbach"). Wird automatisch aufgelöst – bei Bedarf per search-cities den genauen Namen ermitteln.'),
|
||||||
|
'city_id' => $schema->integer()->description('Optional: ID der Stadt, falls bereits bekannt (Alternative zu "city").'),
|
||||||
'name' => $schema->string()->description('Name des Veranstaltungsorts.')->required(),
|
'name' => $schema->string()->description('Name des Veranstaltungsorts.')->required(),
|
||||||
'street' => $schema->string()->description('Straße und Hausnummer des Veranstaltungsorts.')->required(),
|
'street' => $schema->string()->description('Straße und Hausnummer des Veranstaltungsorts.')->required(),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Mcp\Tools\Venue;
|
namespace App\Mcp\Tools\Venue;
|
||||||
|
|
||||||
use App\Http\Resources\VenueResource;
|
use App\Http\Resources\VenueResource;
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
use App\Models\Venue;
|
use App\Models\Venue;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
@@ -14,15 +15,17 @@ use Laravel\Mcp\Server\Tool;
|
|||||||
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||||
|
|
||||||
#[IsReadOnly]
|
#[IsReadOnly]
|
||||||
#[Description('Zeigt einen einzelnen, vom authentifizierten Nutzer erstellten Veranstaltungsort.')]
|
#[Description('Zeigt einen deiner Veranstaltungsorte (per Name angegeben).')]
|
||||||
class ShowMyVenueTool extends Tool
|
class ShowMyVenueTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$venue = Venue::find($request->get('id'));
|
$venue = $this->resolveOwnedByName($request, Venue::class, 'Veranstaltungsorte', 'venue');
|
||||||
|
|
||||||
if (! $venue) {
|
if ($venue instanceof Response) {
|
||||||
return Response::error('Veranstaltungsort nicht gefunden.');
|
return $venue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -40,7 +43,8 @@ class ShowMyVenueTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $schema->integer()->description('ID des Veranstaltungsorts.')->required(),
|
'venue' => $schema->string()->description('Name des Veranstaltungsorts (aus deinen Orten, siehe list-my-venues).'),
|
||||||
|
'id' => $schema->integer()->description('Optional: ID des Veranstaltungsorts, falls bereits bekannt (Alternative zu "venue").'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Mcp\Tools\Venue;
|
|||||||
|
|
||||||
use App\Http\Requests\Api\UpdateVenueRequest;
|
use App\Http\Requests\Api\UpdateVenueRequest;
|
||||||
use App\Http\Resources\VenueResource;
|
use App\Http\Resources\VenueResource;
|
||||||
|
use App\Mcp\Tools\Concerns\ResolvesEntities;
|
||||||
|
use App\Models\City;
|
||||||
use App\Models\Venue;
|
use App\Models\Venue;
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\JsonSchema\Types\Type;
|
use Illuminate\JsonSchema\Types\Type;
|
||||||
@@ -13,15 +15,17 @@ use Laravel\Mcp\Response;
|
|||||||
use Laravel\Mcp\Server\Attributes\Description;
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
use Laravel\Mcp\Server\Tool;
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
#[Description('Aktualisiert einen bestehenden Veranstaltungsort (Venue). Nur der Ersteller oder ein Super-Admin darf ihn ändern.')]
|
#[Description('Aktualisiert einen deiner Veranstaltungsorte (per Name angegeben). Nur der Ersteller oder ein Super-Admin darf ihn ändern.')]
|
||||||
class UpdateVenueTool extends Tool
|
class UpdateVenueTool extends Tool
|
||||||
{
|
{
|
||||||
|
use ResolvesEntities;
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$venue = Venue::find($request->get('id'));
|
$venue = $this->resolveOwnedByName($request, Venue::class, 'Veranstaltungsorte', 'venue');
|
||||||
|
|
||||||
if (! $venue) {
|
if ($venue instanceof Response) {
|
||||||
return Response::error('Veranstaltungsort nicht gefunden.');
|
return $venue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@@ -30,6 +34,10 @@ class UpdateVenueTool extends Tool
|
|||||||
return Response::error('Nur der Ersteller oder ein Super-Admin darf diesen Veranstaltungsort ändern.');
|
return Response::error('Nur der Ersteller oder ein Super-Admin darf diesen Veranstaltungsort ändern.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($error = $this->mergeForeignKey($request, 'city', 'city_id', City::query(), 'Stadt', false)) {
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validate((new UpdateVenueRequest)->rules());
|
$validated = $request->validate((new UpdateVenueRequest)->rules());
|
||||||
|
|
||||||
$venue->update($validated);
|
$venue->update($validated);
|
||||||
@@ -43,9 +51,11 @@ class UpdateVenueTool extends Tool
|
|||||||
public function schema(JsonSchema $schema): array
|
public function schema(JsonSchema $schema): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $schema->integer()->description('ID des zu aktualisierenden Veranstaltungsorts.')->required(),
|
'venue' => $schema->string()->description('Name des zu ändernden Veranstaltungsorts (aus deinen Orten, siehe list-my-venues).'),
|
||||||
'city_id' => $schema->integer()->description('ID der zugehörigen Stadt.'),
|
'id' => $schema->integer()->description('Optional: ID des Veranstaltungsorts, falls bereits bekannt (Alternative zu "venue").'),
|
||||||
'name' => $schema->string()->description('Name des Veranstaltungsorts.'),
|
'city' => $schema->string()->description('Name der zugehörigen Stadt (wird automatisch aufgelöst).'),
|
||||||
|
'city_id' => $schema->integer()->description('Optional: ID der Stadt (Alternative zu "city").'),
|
||||||
|
'name' => $schema->string()->description('Neuer Name des Veranstaltungsorts.'),
|
||||||
'street' => $schema->string()->description('Straße und Hausnummer des Veranstaltungsorts.'),
|
'street' => $schema->string()->description('Straße und Hausnummer des Veranstaltungsorts.'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Mcp\Servers\EinundzwanzigServer;
|
||||||
|
use App\Mcp\Tools\Meetup\CreateMeetupTool;
|
||||||
|
use App\Mcp\Tools\Meetup\UpdateMeetupTool;
|
||||||
|
use App\Mcp\Tools\MeetupEvent\CreateMeetupEventTool;
|
||||||
|
use App\Models\City;
|
||||||
|
use App\Models\Meetup;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
it('creates a meetup event by meetup name instead of id', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$meetup = Meetup::factory()->create(['created_by' => $user->id, 'name' => 'Einundzwanzig Ansbach']);
|
||||||
|
|
||||||
|
EinundzwanzigServer::actingAs($user)
|
||||||
|
->tool(CreateMeetupEventTool::class, [
|
||||||
|
'meetup' => 'Einundzwanzig Ansbach',
|
||||||
|
'start' => '2026-08-01 18:00:00',
|
||||||
|
])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('meetup_events', [
|
||||||
|
'meetup_id' => $meetup->id,
|
||||||
|
'created_by' => $user->id,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the list of own meetups when the meetup name is unknown', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
Meetup::factory()->create(['created_by' => $user->id, 'name' => 'Einundzwanzig Ansbach']);
|
||||||
|
Meetup::factory()->create(['created_by' => $user->id, 'name' => 'Plan B Lugano']);
|
||||||
|
|
||||||
|
EinundzwanzigServer::actingAs($user)
|
||||||
|
->tool(CreateMeetupEventTool::class, [
|
||||||
|
'meetup' => 'Gibt es nicht',
|
||||||
|
'start' => '2026-08-01 18:00:00',
|
||||||
|
])
|
||||||
|
->assertHasErrors()
|
||||||
|
->assertSee('Einundzwanzig Ansbach')
|
||||||
|
->assertSee('Plan B Lugano');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches the meetup name case-insensitively', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$meetup = Meetup::factory()->create(['created_by' => $user->id, 'name' => 'Einundzwanzig Ansbach']);
|
||||||
|
|
||||||
|
EinundzwanzigServer::actingAs($user)
|
||||||
|
->tool(CreateMeetupEventTool::class, [
|
||||||
|
'meetup' => 'einundzwanzig ansbach',
|
||||||
|
'start' => '2026-08-01 18:00:00',
|
||||||
|
])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('meetup_events', ['meetup_id' => $meetup->id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a meetup selected by name', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
Meetup::factory()->create(['created_by' => $user->id, 'name' => 'Altname']);
|
||||||
|
|
||||||
|
EinundzwanzigServer::actingAs($user)
|
||||||
|
->tool(UpdateMeetupTool::class, ['meetup' => 'Altname', 'name' => 'Neuname'])
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Neuname');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('meetups', ['name' => 'Neuname', 'created_by' => $user->id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a meetup resolving the city by name', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$city = City::factory()->create(['name' => 'Ansbach']);
|
||||||
|
|
||||||
|
EinundzwanzigServer::actingAs($user)
|
||||||
|
->tool(CreateMeetupTool::class, ['name' => 'Einundzwanzig Ansbach', 'city' => 'Ansbach'])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('meetups', [
|
||||||
|
'name' => 'Einundzwanzig Ansbach',
|
||||||
|
'city_id' => $city->id,
|
||||||
|
'created_by' => $user->id,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports an unknown city name when creating a meetup', function () {
|
||||||
|
EinundzwanzigServer::actingAs(User::factory()->create())
|
||||||
|
->tool(CreateMeetupTool::class, ['name' => 'Test', 'city' => 'Gibtsnicht'])
|
||||||
|
->assertHasErrors()
|
||||||
|
->assertSee('nicht gefunden');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user