diff --git a/docs-v3/assets/css/main.css b/docs-v3/assets/css/main.css index 57b3056ce..0265eca0d 100644 --- a/docs-v3/assets/css/main.css +++ b/docs-v3/assets/css/main.css @@ -37,6 +37,63 @@ pre { @apply overflow-x-auto p-4 rounded-lg; } +/* Shiki code blocks - light theme improvements */ +.shiki { + @apply bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg; +} + +.shiki pre { + @apply bg-transparent m-0 p-4; +} + +/* Override the dark background in light mode */ +.shiki.shiki-themes.github-light.github-dark { + @apply bg-gray-50 dark:bg-gray-900; +} + +.shiki code { + @apply bg-transparent text-sm leading-relaxed; +} + +/* Fix unstyled content in HTTP and other code blocks */ +.shiki code span:not([style]), +.shiki code span[style=""], +.shiki code span:empty, +.shiki code span { + @apply text-gray-800 dark:text-gray-200; +} + +/* Specific styling for HTTP code blocks */ +.language-http .shiki, +.language-http.shiki { + @apply bg-blue-50 dark:bg-gray-900; +} + +.language-http code, +.language-http .shiki code, +.language-http span { + @apply text-blue-900 dark:text-blue-200 font-semibold; +} + +/* Override for all code block content to ensure visibility */ +.prose .shiki code, +.prose .language-http code, +.prose pre code { + @apply text-gray-900 dark:text-gray-100; +} + +/* Force visibility for any unstyled spans in code */ +.prose .shiki code span, +.prose .language-http code span, +.prose pre code span { + @apply text-gray-900 dark:text-gray-100; +} + +/* Special handling for HTTP method blocks */ +.language-http.shiki code span { + @apply text-blue-900 dark:text-blue-200 font-semibold !important; +} + /* Custom components */ .prose-docs { @apply prose prose-gray max-w-none dark:prose-dark; @@ -57,12 +114,12 @@ pre { } .prose-docs code { - @apply text-sm font-mono bg-blue-100 dark:bg-gray-800 text-blue-900 dark:text-blue-400 px-1 py-0.5 rounded; + @apply text-sm font-mono bg-blue-200 dark:bg-gray-700 text-blue-900 dark:text-blue-300 px-2 py-1 rounded; } /* Inline code styling */ .prose-docs :not(pre) > code { - @apply bg-blue-100 dark:bg-gray-800 text-blue-900 dark:text-blue-400 px-1 py-0.5 rounded font-medium; + @apply bg-blue-200 dark:bg-gray-700 text-blue-900 dark:text-blue-300 px-2 py-1 rounded font-medium; } /* Navigation active states */ diff --git a/docs-v3/components/docs/DocsSidebar.vue b/docs-v3/components/docs/DocsSidebar.vue new file mode 100644 index 000000000..8f82f195a --- /dev/null +++ b/docs-v3/components/docs/DocsSidebar.vue @@ -0,0 +1,250 @@ + + + \ No newline at end of file diff --git a/docs-v3/components/docs/DocsTableOfContents.vue b/docs-v3/components/docs/DocsTableOfContents.vue new file mode 100644 index 000000000..bd3592f0f --- /dev/null +++ b/docs-v3/components/docs/DocsTableOfContents.vue @@ -0,0 +1,75 @@ + + + \ No newline at end of file diff --git a/docs-v3/components/website/WebsiteFooter.vue b/docs-v3/components/website/WebsiteFooter.vue new file mode 100644 index 000000000..654abb405 --- /dev/null +++ b/docs-v3/components/website/WebsiteFooter.vue @@ -0,0 +1,183 @@ + + + \ No newline at end of file diff --git a/docs-v3/components/website/WebsiteNavbar.vue b/docs-v3/components/website/WebsiteNavbar.vue new file mode 100644 index 000000000..5d54031f9 --- /dev/null +++ b/docs-v3/components/website/WebsiteNavbar.vue @@ -0,0 +1,177 @@ + + + \ No newline at end of file diff --git a/docs-v3/content/api/actions.md b/docs-v3/content/docs/api/actions.md similarity index 100% rename from docs-v3/content/api/actions.md rename to docs-v3/content/docs/api/actions.md diff --git a/docs-v3/content/api/fields.md b/docs-v3/content/docs/api/fields.md similarity index 100% rename from docs-v3/content/api/fields.md rename to docs-v3/content/docs/api/fields.md diff --git a/docs-v3/content/api/getters.md b/docs-v3/content/docs/api/getters.md similarity index 99% rename from docs-v3/content/api/getters.md rename to docs-v3/content/docs/api/getters.md index eeb350d6d..9c717fa63 100644 --- a/docs-v3/content/api/getters.md +++ b/docs-v3/content/docs/api/getters.md @@ -2,7 +2,7 @@ title: Getters menuTitle: Getters category: API -position: 10 +position: 11 --- ## Motivation diff --git a/docs-v3/content/api/relations.md b/docs-v3/content/docs/api/relations.md similarity index 100% rename from docs-v3/content/api/relations.md rename to docs-v3/content/docs/api/relations.md diff --git a/docs-v3/content/api/repositories-advanced.md b/docs-v3/content/docs/api/repositories-advanced.md similarity index 60% rename from docs-v3/content/api/repositories-advanced.md rename to docs-v3/content/docs/api/repositories-advanced.md index cfee4c1e3..4c2dc969e 100644 --- a/docs-v3/content/api/repositories-advanced.md +++ b/docs-v3/content/docs/api/repositories-advanced.md @@ -1,13 +1,25 @@ --- -title: Repositories advanced +title: Advanced Repositories menuTitle: Advanced category: API position: 7 --- -## Query Builder +This guide covers advanced repository features for experienced Laravel Restify users. If you're new to Restify, start with the [Basic Repositories](/api/repositories-basic) guide. -To perform any request to the database, Restify has to create a query builder instance. The query builder is passed through a few static methods from the repository, so you can override them and intercept the builder to add your custom statements. +## Overview + +Advanced repository features include: +- **Query Customization** - Control how data is fetched from the database +- **Custom Field Methods** - Different fields for different operations +- **Public Repositories** - Allow unauthenticated access +- **Repository Lifecycle** - Hook into CRUD operations +- **Custom Routes** - Add your own endpoints +- **Performance Optimization** - Bulk operations, caching, eager loading + +## Query Customization + +Restify provides several methods to customize how data is queried from your database. These methods are called in a specific order, allowing you to build complex query logic. ### Main query @@ -517,17 +529,348 @@ the `routes` method. You should be careful about this behavior. +## Advanced Field Methods + +While the basic `fields()` method works for most cases, you can define different fields for different operations: + +```php +class PostRepository extends Repository +{ + // Default fields used for all operations + public function fields(RestifyRequest $request): array + { + return [ + field('title'), + field('content'), + field('excerpt'), + field('published_at'), + field('created_at'), + field('updated_at'), + ]; + } + + // Lighter fields for listing operations + public function fieldsForIndex(RestifyRequest $request): array + { + return [ + field('title'), + field('excerpt'), + field('published_at'), + ]; + } + + // Full detail fields for individual resources + public function fieldsForShow(RestifyRequest $request): array + { + return [ + field('title'), + field('content'), + field('excerpt'), + field('published_at'), + field('author_name', fn() => $this->author->name), + ]; + } + + // Only allow certain fields to be created + public function fieldsForStore(RestifyRequest $request): array + { + return [ + field('title')->required(), + field('content')->required(), + field('excerpt'), + ]; + } + + // Only allow certain fields to be updated + public function fieldsForUpdate(RestifyRequest $request): array + { + return [ + field('title'), + field('content'), + field('excerpt'), + field('published_at'), + ]; + } +} +``` + +### Field Method Priority + +Restify uses this priority order when determining which fields to use: +1. **Operation-specific method** (`fieldsForIndex`, `fieldsForShow`, etc.) - **Highest priority** +2. **Default fields method** (`fields`) - **Fallback** + +## Public Repositories + +Sometimes you need to expose certain repositories without authentication (e.g., for a public blog or documentation site). + + +Use public repositories carefully. Consider using the [serializer](/api/serializer) for custom public endpoints instead. + + +```php +class PostRepository extends Repository +{ + // Allow public access + public static bool $public = true; + + public function fields(RestifyRequest $request): array + { + return [ + field('title'), + field('content'), + field('published_at'), + ]; + } +} +``` + +### Public Repository Setup + +**1. Update your global gate to allow null users:** + +```php +// app/Providers/RestifyApplicationServiceProvider.php +protected function gate(): void +{ + Gate::define('viewRestify', function ($user = null) { + if (is_null($user)) { + return true; // Allow public access + } + + return in_array($user->email, [...]); + }); +} +``` + +**2. Update your policies to allow null users:** + +```php +// app/Policies/PostPolicy.php +public function allowRestify(User $user = null): bool +{ + return true; // Allow all users (authenticated or not) +} + +public function view(User $user = null, Post $post): bool +{ + return $post->status === 'published'; // Only show published posts +} +``` + +## Repository Collections and Transforms + +### Transform Collections Before Serialization + +```php +class PostRepository extends Repository +{ + public function indexCollection(RestifyRequest $request, Collection $items): Collection + { + // Filter out unpublished posts + return $items->filter(function ($post) { + return $post->published_at <= now(); + }); + } +} +``` + +### Custom Serialization + +Take complete control over how your resources are serialized: + +```php +class PostRepository extends Repository +{ + public function serializeForIndex(RestifyRequest $request): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'excerpt' => Str::limit($this->content, 100), + 'read_time' => $this->calculateReadTime(), + 'url' => route('posts.show', $this->slug), + ]; + } + + public function serializeForShow(RestifyRequest $request): array + { + $data = parent::serializeForShow($request); + + // Add computed fields + $data['computed'] = [ + 'word_count' => str_word_count(strip_tags($this->content)), + 'reading_time' => ceil(str_word_count($this->content) / 200), + 'related_posts' => $this->getRelatedPosts(3), + ]; + + return $data; + } +} +``` + +## Repository Labels and Identifiers + +### Custom Labels + +Customize how your repository appears in API documentation: + +```php +class PostRepository extends Repository +{ + public static string $label = 'Blog Articles'; + + // Or dynamically + public static function label(): string + { + return __('repository.posts'); + } +} +``` + +### Title and Subtitle Fields + +```php +class PostRepository extends Repository +{ + public static string $title = 'title'; // Default is 'id' + + public function title(): string + { + return $this->title ?: "Post #{$this->id}"; + } + + public function subtitle(): ?string + { + return "By {$this->author->name} on {$this->published_at->format('M j, Y')}"; + } +} +``` + +### Custom URI Keys + +```php +class PostRepository extends Repository +{ + // Use 'articles' instead of 'posts' in URLs + public static string $uriKey = 'articles'; + + // Or dynamically + public static function uriKey(): string + { + return config('app.locale') === 'es' ? 'articulos' : 'articles'; + } +} +``` + +## Repository Lifecycle and Events + +Hook into repository operations to perform additional logic: + +### Single Resource Events + +```php +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Cache; + +class PostRepository extends Repository +{ + // Called after successfully creating a resource + public static function stored($model, $request) + { + Log::info("Post created: {$model->title}"); + Cache::tags(['posts'])->flush(); + $model->searchable(); // Add to search index + } + + // Called after successfully updating a resource + public static function updated($model, $request) + { + $dirty = $model->getDirty(); + Log::info("Post updated: {$model->title}", [ + 'changed_fields' => array_keys($dirty), + ]); + + if (isset($dirty['content']) || isset($dirty['title'])) { + $model->searchable(); // Re-index if content changed + } + } + + // Called after successfully deleting a resource + public static function deleted($status, $request) + { + if ($status) { + Cache::tags(['posts'])->flush(); + } + } +} +``` + +### Bulk Operation Events + +```php +class PostRepository extends Repository +{ + public static function storedBulk(Collection $models, $request) + { + Log::info("Bulk created {$models->count()} posts"); + $models->searchable(); // Bulk index for search + } + + public static function updatedBulk(Collection $models, $request) + { + Cache::tags(['posts'])->flush(); + } +} +``` + +### Authorization Hooks + +Custom authorization logic beyond policies: + +```php +class PostRepository extends Repository +{ + public function allowToShow($request): self + { + if (!$this->model()->isPublished() && !$request->user()->isAdmin()) { + throw new AuthorizationException('Cannot view unpublished post'); + } + + return $this; + } + + public function allowToStore(RestifyRequest $request, $payload = null): self + { + if ($request->user()->posts()->today()->count() >= 10) { + throw new AuthorizationException('Daily post limit reached'); + } + + return $this; + } + + public function allowToUpdate(RestifyRequest $request, $payload = null): self + { + if ($this->model()->isPublished() && !$request->user()->isEditor()) { + throw new AuthorizationException('Cannot edit published posts'); + } + + return $this; + } +} +``` + ## Repository Lifecycle -Each repository has several lifecycle methods. The most useful is `booted`, which is called as soon as the repository is loaded with the resource: +Each repository has several lifecycle methods. The most useful is `booted`, which is called as soon as the repository is loaded: -````php +```php // PostRepository.php protected static function booted() { - // + // Initialization logic here } -```` +``` diff --git a/docs-v3/content/docs/api/repositories-basic.md b/docs-v3/content/docs/api/repositories-basic.md new file mode 100644 index 000000000..b435ff197 --- /dev/null +++ b/docs-v3/content/docs/api/repositories-basic.md @@ -0,0 +1,368 @@ +--- +title: Basic Repositories +menuTitle: Basic Repositories +category: API +position: 4 +--- + +The Repository is the core of Laravel Restify. It defines how your models are exposed through API endpoints, handling CRUD operations automatically while giving you full control over fields, validation, and authorization. + +## Quick Start + +Create a repository with the Artisan command: + +```bash +php artisan restify:repository PostRepository +``` + +This creates `app/Restify/PostRepository.php` associated with your `Post` model. + +## Basic Repository + +Here's a minimal repository to get you started: + +```php +namespace App\Restify; + +use App\Models\Post; +use Binaryk\LaravelRestify\Repositories\Repository; +use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; +use Binaryk\LaravelRestify\Attributes\Model; + +#[Model(Post::class)] +class PostRepository extends Repository +{ + public function fields(RestifyRequest $request): array + { + return [ + field('title')->required(), + field('content'), + field('published_at')->nullable(), + ]; + } +} +``` + +That's it! You now have a complete API with these endpoints: + +| Method | URL | Action | +|--------|-----|---------| +| GET | `/api/restify/posts` | List all posts | +| GET | `/api/restify/posts/1` | Get a specific post | +| POST | `/api/restify/posts` | Create a new post | +| PUT | `/api/restify/posts/1` | Update a post | +| DELETE | `/api/restify/posts/1` | Delete a post | + +## Model Association + +Restify needs to know which Eloquent model your repository represents. There are three ways to define this: + +### 1. Modern Approach (Recommended) + +Use PHP 8+ attributes: + +```php +#[Model(Post::class)] +class PostRepository extends Repository +{ + // Fields... +} +``` + +### 2. Static Property + +```php +class PostRepository extends Repository +{ + public static string $model = Post::class; + + // Fields... +} +``` + +### 3. Auto-Guessing + +If you don't specify a model, Restify will guess it from the repository name: +- `PostRepository` → `App\Models\Post` +- `BlogPostRepository` → `App\Models\BlogPost` + +## Fields + +The `fields()` method defines which model attributes are exposed through your API: + +```php +public function fields(RestifyRequest $request): array +{ + return [ + field('title') + ->required() + ->rules('max:255'), + + field('content') + ->rules('required'), + + field('status') + ->default('draft'), + + field('published_at') + ->nullable(), + ]; +} +``` + +### Field Types + +Common field patterns for different data: + +```php +field('title')->string(), // Text with string validation +field('content')->string(), // Content field +textarea('content'), // Long text (helper function) +field('price')->numeric(), // Numbers +field('published')->boolean(), // True/false +field('published_at')->date(), // Dates +field('status')->string(), // Status field +``` + +### Field Validation + +Add Laravel validation rules to fields: + +```php +field('email') + ->required() + ->rules('email', 'unique:users,email'), + +field('age') + ->rules('integer', 'min:18'), +``` + +## CRUD Operations + +### Creating Posts + +**Request:** +```bash +POST /api/restify/posts +Content-Type: application/json + +{ + "title": "My First Post", + "content": "Hello World!" +} +``` + +**Response:** +```json +{ + "data": { + "id": "1", + "type": "posts", + "attributes": { + "title": "My First Post", + "content": "Hello World!", + "published_at": null + } + } +} +``` + +### Reading Posts + +**List all posts:** +```bash +GET /api/restify/posts +``` + +**Get specific post:** +```bash +GET /api/restify/posts/1 +``` + +### Updating Posts + +```bash +PUT /api/restify/posts/1 +Content-Type: application/json + +{ + "title": "Updated Title", + "published_at": "2024-01-15T10:00:00Z" +} +``` + +### Deleting Posts + +```bash +DELETE /api/restify/posts/1 +``` + +Returns `204 No Content` on success. + +## Relationships + +Define relationships in your repository to work with related models: + +```php +use Binaryk\LaravelRestify\Fields\BelongsTo; +use Binaryk\LaravelRestify\Fields\HasMany; + +class PostRepository extends Repository +{ + public function fields(RestifyRequest $request): array + { + return [ + field('title'), + field('content'), + ]; + } + + public static function related(): array + { + return [ + 'author' => BelongsTo::make('user', UserRepository::class), + 'comments' => HasMany::make('comments', CommentRepository::class), + ]; + } +} +``` + +### Loading Relationships + +Include related data in your API responses: + +```bash +GET /api/restify/posts?related=author,comments +``` + +## Search and Filtering + +Make fields searchable, sortable, or filterable: + +```php +public function fields(RestifyRequest $request): array +{ + return [ + field('title') + ->searchable() // Can search by title + ->sortable(), // Can sort by title + + field('status') + ->matchable(), // Can filter by exact status + + field('content') + ->searchable(), // Can search in content + ]; +} +``` + +### Using Search and Filters + +```bash +# Search for posts containing "laravel" +GET /api/restify/posts?search=laravel + +# Filter by status +GET /api/restify/posts?status=published + +# Sort by title +GET /api/restify/posts?sort=title + +# Sort descending +GET /api/restify/posts?sort=-title + +# Combine multiple parameters +GET /api/restify/posts?search=laravel&status=published&sort=-created_at +``` + +## Pagination + +All index requests are paginated automatically: + +```bash +# Get page 2 with 10 items per page +GET /api/restify/posts?page=2&perPage=10 +``` + +## Authorization + +Protect your repositories with Laravel policies: + +```php +// app/Policies/PostPolicy.php +class PostPolicy +{ + public function allowRestify(User $user = null): bool + { + return true; + } + + public function show(User $user = null, Post $post): bool + { + return true; + } + + public function store(User $user): bool + { + return $user->hasRole('editor'); + } + + public function update(User $user, Post $post): bool + { + return $user->id === $post->user_id || $user->hasRole('admin'); + } + + public function delete(User $user, Post $post): bool + { + return $user->hasRole('admin'); + } +} +``` + +Register your policy in `AuthServiceProvider`: + +```php +protected $policies = [ + Post::class => PostPolicy::class, +]; +``` + +## AI Integration (MCP) + +Laravel Restify includes built-in support for AI agents through Model Context Protocol (MCP). To enable AI access to your repository, add the `HasMcpTools` trait: + +```php +use Binaryk\LaravelRestify\Traits\HasMcpTools; + +#[Model(Post::class)] +class PostRepository extends Repository +{ + use HasMcpTools; + + public function fields(RestifyRequest $request): array + { + return [ + field('title')->required(), + field('content'), + field('published_at')->nullable(), + ]; + } +} +``` + +This automatically creates MCP tools that AI agents can use to: +- List your posts +- Read specific posts +- Create new posts (if you enable it) +- Update posts (if you enable it) + +For detailed MCP configuration, see the [MCP Repositories](/docs/mcp/repositories) documentation. + +## What's Next? + +You now know the basics of Laravel Restify repositories! For more advanced features, check out: + +- **[Advanced Repositories](/docs/api/repositories-advanced)** - Query customization, lifecycle events, custom routes +- **[Fields](/docs/api/fields)** - Advanced field types and customization +- **[Authorization](/docs/auth/authorization)** - Advanced security and permissions +- **[MCP Integration](/docs/mcp/repositories)** - Full AI agent integration diff --git a/docs-v3/content/api/repositories.md b/docs-v3/content/docs/api/repositories.md similarity index 88% rename from docs-v3/content/api/repositories.md rename to docs-v3/content/docs/api/repositories.md index 0880292cc..a97980289 100644 --- a/docs-v3/content/api/repositories.md +++ b/docs-v3/content/docs/api/repositories.md @@ -1,11 +1,26 @@ --- -title: Repositories -menuTitle: Repositories +title: Repository Overview +menuTitle: Overview category: API -position: 6 +position: 5 --- -The Repository is the core of the Laravel Restify. +The Repository is the core of Laravel Restify, providing a unified API layer that serves both human users via REST endpoints and AI agents via MCP tools. + +## Documentation Structure + +**New to Laravel Restify?** Start with the basics: +- **[Basic Repositories](/docs/api/repositories-basic)** - Essential concepts and getting started guide + +**Ready for advanced features?** +- **[Advanced Repositories](/docs/api/repositories-advanced)** - Query customization, lifecycle events, custom serialization +- **[MCP Integration](/docs/mcp/repositories)** - AI agent integration and Model Context Protocol + +--- + +*This page contains the original comprehensive documentation. For a better learning experience, we recommend starting with the [Basic Repositories](/docs/api/repositories-basic) guide.* + +--- ## Quick start @@ -29,7 +44,8 @@ The basic repository form looks like this using the modern attribute approach: namespace App\Restify; use App\Models\Post; -use App\Restify\Repository; +use Binaryk\LaravelRestify\Repositories\Repository; +use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Binaryk\LaravelRestify\Attributes\Model; #[Model(Post::class)] @@ -37,30 +53,14 @@ class PostRepository extends Repository { public function fields(RestifyRequest $request): array { - return []; - } -} -``` - -Or using the traditional static property approach: - -```php -namespace App\Restify; - -use App\Models\Post; -use App\Restify\Repository; - -class PostRepository extends Repository -{ - public static string $model = Post::class; - - public function fields(RestifyRequest $request): array - { - return []; + return [ + field('title')->required(), + field('content')->string(), + field('published_at')->nullable(), + ]; } } ``` - If you don't specify the model using an attribute or the $model property, Restify will try to guess the model automatically based on the repository class name. @@ -152,13 +152,6 @@ class PostRepository extends Repository } ``` -**Benefits of using attributes:** -- Modern, declarative approach -- Better IDE support and static analysis -- Cleaner code (no need for static properties) -- More discoverable with reflection tools -- Type-safe when using `::class` syntax - ### 2. Traditional Approach: Static Property The classic approach using static properties (still fully supported): @@ -297,21 +290,49 @@ your API will be as private as possible. To some extent, `fields` are similar to the `toArray` method from the [laravel resource](https://laravel.com/docs/eloquent-resources#concept-overview) concept. -Let's define some fields for our Post model: +Let's define some comprehensive fields for our Post model: ```php -use Binaryk\LaravelRestify\Fields\Field; use Binaryk\LaravelRestify\Repositories\Repository; use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; class PostRepository extends Repository { - public function fields(RestifyRequest $request) + public function fields(RestifyRequest $request): array { return [ - field('title'), + field('title') + ->rules('required', 'max:255') + ->sortable() + ->matchable(), + + field('slug') + ->rules('required', 'unique:posts,slug') + ->hideFromIndex(), + + field('content') + ->textarea() + ->rules('required', 'min:100') + ->searchable(), + + field('excerpt') + ->nullable() + ->hideFromIndex(), + + field('status') + ->select(['draft', 'published', 'archived']) + ->default('draft') + ->sortable() + ->matchable(), + + field('published_at') + ->nullable() + ->sortable(), - field('description'), + field('featured') + ->boolean() + ->default(false) + ->matchable(), ]; } } @@ -584,6 +605,8 @@ public function fieldsForIndex(RestifyRequest $request): array Specific fields per request type could be defined for other requests. For example: `fieldsForIndex`, `fieldsForShow`, `fieldsForStore` and `fieldsForUpdate`. +For AI agents, you can also define MCP-specific field methods like `fieldsForMcpIndex`, `fieldsForMcpShow`, etc. See the [MCP Repositories](/docs/mcp/repositories) documentation for details on optimizing repositories for AI agent consumption. + ## Store request @@ -1118,129 +1141,6 @@ class PostRepository extends Repository } ``` -## MCP Integration - -Laravel Restify provides first-class support for Model Context Protocol (MCP), allowing AI agents to efficiently interact with your APIs. You can define MCP-specific field methods to optimize token usage and provide tailored data for AI consumption. - -### MCP Field Methods - -MCP field methods follow the same pattern as regular field methods but are prefixed with `fieldsForMcp`: - -```php -class PostRepository extends Repository -{ - // Regular fields for human consumption - public function fields(RestifyRequest $request): array - { - return [ - field('title'), - field('content'), - field('excerpt'), - field('meta_description'), - field('tags'), - field('author_id'), - field('published_at'), - field('created_at'), - field('updated_at'), - ]; - } - - // Optimized fields for AI index requests (saves 60-70% tokens) - public function fieldsForMcpIndex(RestifyRequest $request): array - { - return [ - field('id'), - field('title'), - field('excerpt'), - field('published_at'), - ]; - } - - // Focused fields for AI detail views (saves 40-50% tokens) - public function fieldsForMcpShow(RestifyRequest $request): array - { - return [ - field('title'), - field('content'), - field('author', fn() => $this->author->name), - field('tags'), - field('published_at'), - ]; - } - - // Fields AI agents can use for creation - public function fieldsForMcpStore(RestifyRequest $request): array - { - return [ - field('title')->required(), - field('content')->required(), - field('excerpt'), - field('tags'), - ]; - } - - // Fields AI agents can modify - public function fieldsForMcpUpdate(RestifyRequest $request): array - { - return [ - field('title'), - field('content'), - field('excerpt'), - field('tags'), - ]; - } -} -``` - -### MCP Bulk Operations - -```php -// Efficient AI bulk creation -public function fieldsForMcpStoreBulk(RestifyRequest $request): array -{ - return [ - field('title')->required(), - field('content')->required(), - field('status')->value('draft'), - ]; -} - -// Efficient AI bulk updates -public function fieldsForMcpUpdateBulk(RestifyRequest $request): array -{ - return [ - field('title'), - field('status'), - field('published_at'), - ]; -} -``` - -### MCP Getters - -Provide analytical and computed fields specifically for AI consumption: - -```php -public function fieldsForMcpGetter(RestifyRequest $request): array -{ - return [ - field('word_count', fn() => str_word_count(strip_tags($this->content))), - field('reading_time', fn() => ceil(str_word_count(strip_tags($this->content)) / 200)), - field('sentiment_score', fn() => $this->calculateSentiment()), - field('related_topics', fn() => $this->extractTopics()), - ]; -} -``` - -### Field Priority for MCP - -When an MCP request is made, Restify follows this priority order: - -1. **MCP-specific methods** (`fieldsForMcpIndex`, `fieldsForMcpShow`, etc.) -2. **Request-specific methods** (`fieldsForIndex`, `fieldsForShow`, etc.) -3. **Default fields method** (`fields`) - -This allows you to provide optimized field sets for AI agents while maintaining full functionality for human users. ## Repository Lifecycle Events @@ -1249,43 +1149,78 @@ Laravel Restify provides several lifecycle hooks that allow you to perform actio ### Single Resource Events ```php +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Cache; + class PostRepository extends Repository { // Called after a single resource is successfully stored public static function stored($model, $request) { - // Log the creation - Log::info("Post created: {$model->title}"); + // Log the creation with context + Log::info("Post created: {$model->title}", [ + 'post_id' => $model->id, + 'user_id' => $request->user()->id, + 'ip' => $request->ip(), + ]); - // Send notifications - NotificationService::notifyNewPost($model); + // Send notifications to subscribers + $model->author->notify(new PostPublishedNotification($model)); // Update caches - cache()->forget('recent_posts'); + Cache::tags(['posts', 'recent'])->flush(); + + // Add to search index + $model->searchable(); + + // Update statistics + cache()->increment('posts_count_today'); } // Called after a single resource is successfully updated public static function updated($model, $request) { - // Log the update - Log::info("Post updated: {$model->title}"); + // Log the update with changed fields + $dirty = $model->getDirty(); + Log::info("Post updated: {$model->title}", [ + 'post_id' => $model->id, + 'changed_fields' => array_keys($dirty), + 'user_id' => $request->user()->id, + ]); // Clear related caches - cache()->forget("post_{$model->id}"); + Cache::forget("post_{$model->id}"); + Cache::tags(['posts', $model->slug])->flush(); - // Index for search - $model->searchable(); + // Re-index for search if content changed + if (isset($dirty['content']) || isset($dirty['title'])) { + $model->searchable(); + } + + // Handle status change + if (isset($dirty['status']) && $model->status === 'published') { + event(new PostPublished($model)); + } } // Called after a single resource is successfully deleted public static function deleted($status, $request) { - // Log deletion - Log::info("Post deleted, status: {$status}"); + // Log deletion with context + Log::info("Post deleted", [ + 'status' => $status, + 'user_id' => $request->user()->id, + 'soft_delete' => $request->repository()->resource->trashed() ?? false, + ]); - // Clean up related data + // Clean up related data only on successful deletion if ($status) { - cache()->flush(); + Cache::tags(['posts'])->flush(); + + // Remove from search index + if (method_exists($request->repository()->resource, 'unsearchable')) { + $request->repository()->resource->unsearchable(); + } } } } diff --git a/docs-v3/content/api/repository-generation.md b/docs-v3/content/docs/api/repository-generation.md similarity index 99% rename from docs-v3/content/api/repository-generation.md rename to docs-v3/content/docs/api/repository-generation.md index 9b8755348..655205c05 100644 --- a/docs-v3/content/api/repository-generation.md +++ b/docs-v3/content/docs/api/repository-generation.md @@ -2,7 +2,7 @@ title: Repository Generation menuTitle: Repository Generation category: API -position: 11 +position: 12 --- # Repository Generation diff --git a/docs-v3/content/api/rest-methods.md b/docs-v3/content/docs/api/rest-methods.md similarity index 99% rename from docs-v3/content/api/rest-methods.md rename to docs-v3/content/docs/api/rest-methods.md index 94b8d005b..fa37fbb19 100644 --- a/docs-v3/content/api/rest-methods.md +++ b/docs-v3/content/docs/api/rest-methods.md @@ -2,7 +2,7 @@ title: REST Methods menuTitle: Controllers category: API -position: 12 +position: 13 --- ## Introduction diff --git a/docs-v3/content/api/serializer.md b/docs-v3/content/docs/api/serializer.md similarity index 99% rename from docs-v3/content/api/serializer.md rename to docs-v3/content/docs/api/serializer.md index 3ff60f50e..24e890d2a 100644 --- a/docs-v3/content/api/serializer.md +++ b/docs-v3/content/docs/api/serializer.md @@ -2,7 +2,7 @@ title: Serializer menuTitle: Serializer category: API -position: 12 +position: 14 --- ## Introduction diff --git a/docs-v3/content/api/validation-methods.md b/docs-v3/content/docs/api/validation-methods.md similarity index 99% rename from docs-v3/content/api/validation-methods.md rename to docs-v3/content/docs/api/validation-methods.md index 40d57fbcd..681419d6e 100644 --- a/docs-v3/content/api/validation-methods.md +++ b/docs-v3/content/docs/api/validation-methods.md @@ -2,7 +2,7 @@ title: Validation Methods menuTitle: Validation Methods category: API -position: 9 +position: 15 --- # Fluent Validation Methods diff --git a/docs-v3/content/auth/authentication.md b/docs-v3/content/docs/auth/authentication.md similarity index 100% rename from docs-v3/content/auth/authentication.md rename to docs-v3/content/docs/auth/authentication.md diff --git a/docs-v3/content/auth/authorization.md b/docs-v3/content/docs/auth/authorization.md similarity index 100% rename from docs-v3/content/auth/authorization.md rename to docs-v3/content/docs/auth/authorization.md diff --git a/docs-v3/content/auth/profile.md b/docs-v3/content/docs/auth/profile.md similarity index 100% rename from docs-v3/content/auth/profile.md rename to docs-v3/content/docs/auth/profile.md diff --git a/docs-v3/content/boost/boost.md b/docs-v3/content/docs/boost/boost.md similarity index 100% rename from docs-v3/content/boost/boost.md rename to docs-v3/content/docs/boost/boost.md diff --git a/docs-v3/content/graphql/graphql-generation.md b/docs-v3/content/docs/graphql/graphql-generation.md similarity index 100% rename from docs-v3/content/graphql/graphql-generation.md rename to docs-v3/content/docs/graphql/graphql-generation.md diff --git a/docs-v3/content/graphql/graphql.md b/docs-v3/content/docs/graphql/graphql.md similarity index 100% rename from docs-v3/content/graphql/graphql.md rename to docs-v3/content/docs/graphql/graphql.md diff --git a/docs-v3/content/index.md b/docs-v3/content/docs/index.md similarity index 52% rename from docs-v3/content/index.md rename to docs-v3/content/docs/index.md index e5d17e00b..a7156f91f 100644 --- a/docs-v3/content/index.md +++ b/docs-v3/content/docs/index.md @@ -1,13 +1,15 @@ --- -title: Introduction -description: One Codebase. REST for Humans, MCP for AI Agents. +title: Laravel Restify - PHP REST API Framework & JSON API Generator +description: Transform Laravel Eloquent models into JSON:API endpoints and MCP servers automatically. Complete Laravel API framework with authentication, filtering, and AI agent integration. menuTitle: Introduction category: Getting Started --- -# Unified Laravel API Layer for Humans and AI. +# Laravel REST API Framework & JSON:API Generator -Laravel Restify turns your Eloquent models into both JSON:API endpoints and MCP servers -- automatically. Build once, and instantly serve APIs that work seamlessly for developers, apps, and AI agents. +Laravel Restify is a powerful **Laravel API package** that automatically transforms your Eloquent models into **JSON:API endpoints** and **MCP servers** for AI agents. This comprehensive **PHP REST API framework** enables you to build production-ready APIs with minimal code while following industry standards. + +**Build once, serve everywhere** - Create APIs that work seamlessly for developers, web applications, mobile apps, and AI agents like Claude Desktop. ## Key Features diff --git a/docs-v3/content/docs/mcp/fields.md b/docs-v3/content/docs/mcp/fields.md new file mode 100644 index 000000000..d7cfb93ea --- /dev/null +++ b/docs-v3/content/docs/mcp/fields.md @@ -0,0 +1,377 @@ +--- +title: MCP Fields +menuTitle: Fields +category: MCP +position: 3 +--- + +Laravel Restify fields provide MCP-specific methods to optimize data structures for AI agent consumption, reduce token usage, and control field visibility based on user permissions. + +## MCP Field Methods + +MCP field methods follow the same pattern as regular field methods but are prefixed with `fieldsForMcp`. These methods take priority over regular field methods when handling MCP requests. + +### Field Priority for MCP + +When an MCP request is made, Restify follows this priority order: + +1. **MCP-specific methods** (`fieldsForMcpIndex`, `fieldsForMcpShow`, etc.) - **Highest priority** +2. **Default fields method** (`fields`) - **Fallback** + +This allows you to provide optimized field sets for AI agents while falling back to the standard fields method when no MCP-specific method is defined. + +### Available MCP Field Methods + +```php +class PostRepository extends Repository +{ + // Regular fields for human consumption + public function fields(RestifyRequest $request): array + { + return [ + field('title'), + field('content'), + field('excerpt'), + field('meta_description'), + field('tags'), + field('author_id'), + field('published_at'), + field('created_at'), + field('updated_at'), + ]; + } + + // Optimized fields for AI index requests (saves 60-70% tokens) + public function fieldsForMcpIndex(RestifyRequest $request): array + { + return [ + field('id'), + field('title'), + field('excerpt'), + field('published_at'), + ]; + } + + // Focused fields for AI detail views (saves 40-50% tokens) + public function fieldsForMcpShow(RestifyRequest $request): array + { + return [ + field('title'), + field('content'), + field('author', fn() => $this->author->name), + field('tags'), + field('published_at'), + ]; + } + + // Fields AI agents can use for creation + public function fieldsForMcpStore(RestifyRequest $request): array + { + return [ + field('title')->required(), + field('content')->required(), + field('excerpt'), + field('tags'), + ]; + } + + // Fields AI agents can modify + public function fieldsForMcpUpdate(RestifyRequest $request): array + { + return [ + field('title'), + field('content'), + field('excerpt'), + field('tags'), + ]; + } +``` + +## Token Usage Optimization + +### Index Optimization Example + +**Regular fields method (for humans):** +```php +public function fields(RestifyRequest $request): array +{ + return [ + field('id'), + field('title'), + field('content'), + field('excerpt'), + field('meta_description'), + field('meta_keywords'), + field('author_id'), + field('category_id'), + field('status'), + field('featured'), + field('view_count'), + field('comment_count'), + field('published_at'), + field('created_at'), + field('updated_at'), + ]; // ~15 fields +} +``` + +**MCP optimized for listing (saves ~70% tokens):** +```php +public function fieldsForMcpIndex(RestifyRequest $request): array +{ + return [ + field('id'), + field('title'), + field('excerpt'), + field('published_at'), + field('status'), + ]; // Only 5 essential fields +} +``` + +### Show Optimization Example + +**MCP optimized for detail view (saves ~50% tokens):** +```php +public function fieldsForMcpShow(RestifyRequest $request): array +{ + return [ + field('title'), + field('content'), + field('author_name', fn() => $this->author->name), + field('category', fn() => $this->category->name), + field('tags'), + field('published_at'), + field('status'), + // Removed: meta fields, timestamps, IDs, counts + ]; +} +``` + +## MCP-Aware Relationships + +Handle relationships efficiently for AI agents: + +```php +class PostRepository extends Repository +{ + public static function related(): array + { + return [ + 'author' => BelongsTo::make('user', UserRepository::class), + 'comments' => HasMany::make('comments', CommentRepository::class), + 'tags' => BelongsToMany::make('tags', TagRepository::class), + ]; + } + + // AI agents get optimized relationship data + public function fieldsForMcpShow(RestifyRequest $request): array + { + return [ + field('title'), + field('content'), + // Inline relationship data to reduce API calls + field('author_name', fn() => $this->user->name), + field('author_email', fn() => $this->user->email), + field('comment_count', fn() => $this->comments()->count()), + field('tag_names', fn() => $this->tags->pluck('name')->toArray()), + ]; + } +} +``` + +## Conditional MCP Fields + +Provide different fields based on AI agent context or user permissions using field-level visibility controls: + +### Using canSee() Method + +```php +class PostRepository extends Repository +{ + public function fields(RestifyRequest $request): array + { + return [ + field('title')->required()->searchable()->sortable(), + field('content')->string(), + field('status')->matchable(), + field('category')->matchable(), + + // Admin-only fields + field('internal_notes') + ->canSee(fn($request) => $request->user()->hasRole('admin')), + + field('performance_metrics', fn() => [ + 'views' => $this->view_count, + 'engagement' => $this->calculateEngagement(), + 'conversion_rate' => $this->calculateConversion(), + ])->canSee(fn($request) => $request->user()->hasRole('admin')), + + // Content manager fields + field('author_info', fn() => [ + 'name' => $this->author->name, + 'email' => $this->author->email, + 'posts_count' => $this->author->posts_count, + ])->canSee(fn($request) => $request->user()->hasPermissionTo('manage-content')), + ]; + } +} +``` + +### Using hideFromMcp() Method + +When you want to hide certain fields specifically from MCP requests (while keeping them for REST endpoints), use the `hideFromMcp()` method: + +```php +class PostRepository extends Repository +{ + public function fields(RestifyRequest $request): array + { + return [ + field('title')->required()->searchable()->sortable(), + field('content')->string(), + field('status')->matchable(), + field('category')->matchable(), + + // Hide sensitive data from AI agents + field('internal_notes') + ->hideFromMcp(fn($request) => !$request->user()->hasRole('admin')), + + // Always hide from MCP + field('secret_api_key')->hideFromMcp(), + + // Conditionally hide from MCP based on user role + field('draft_content') + ->hideFromMcp(fn($request) => !$request->user()->hasPermissionTo('view-drafts')), + + // Hide heavy computational fields from MCP to save tokens + field('full_statistics', fn() => $this->generateDetailedStats()) + ->hideFromMcp(), // Use dedicated MCP fields instead + ]; + } + + // Use dedicated MCP field method for optimized AI responses + public function fieldsForMcpShow(RestifyRequest $request): array + { + return [ + field('title'), + field('content'), + field('status'), + field('category'), + // Light-weight stats for AI agents + field('basic_stats', fn() => [ + 'views' => $this->view_count, + 'comments' => $this->comments_count, + ]), + ]; + } +} +``` + +### Benefits of Field-Level Control + +- **No duplicate logic**: Define visibility rules once in the main `fields()` method +- **Automatic application**: Rules apply to all MCP operations (index, show, store, update) +- **Flexible conditions**: Use any logic in the callback to determine visibility +- **Performance optimization**: Hide expensive computed fields from AI agents +- **Security**: Keep sensitive data away from AI agents while maintaining REST functionality + +## Testing MCP Field Methods + +Test your MCP-specific field methods to ensure they work correctly: + +```php +// tests/Feature/McpRepositoryTest.php +class McpRepositoryTest extends TestCase +{ + public function test_mcp_index_fields_are_optimized() + { + $repository = new PostRepository(); + $mcpRequest = new McpRequest(); + $mcpRequest->setIsIndexRequest(true); + + $fields = $repository->collectFields($mcpRequest); + + // Assert only essential fields are returned + $this->assertCount(4, $fields); + $this->assertTrue($fields->contains('attribute', 'title')); + $this->assertTrue($fields->contains('attribute', 'excerpt')); + $this->assertFalse($fields->contains('attribute', 'meta_description')); + } + + public function test_mcp_fields_fall_back_to_regular_fields() + { + $repository = new PostRepository(); + $mcpRequest = new McpRequest(); + + // Remove MCP method to test fallback + $fields = $repository->collectFields($mcpRequest); + + // Should fall back to regular fields method + $this->assertGreaterThan(4, $fields->count()); + } +} +``` + +## MCP Performance Monitoring + +Monitor token usage and performance of your MCP endpoints: + +```php +class PostRepository extends Repository +{ + public function fieldsForMcpIndex(RestifyRequest $request): array + { + $startTime = microtime(true); + + $fields = [ + field('id'), + field('title'), + field('excerpt'), + field('published_at'), + ]; + + // Log performance metrics for AI optimization + Log::info('MCP Index Fields', [ + 'repository' => static::class, + 'field_count' => count($fields), + 'execution_time' => microtime(true) - $startTime, + 'user_agent' => $request->userAgent(), + ]); + + return $fields; + } +} +``` + +## Best Practices + +### 1. Field Selection Strategy + +- **Index**: Include only essential fields for listing (id, title, status, dates) +- **Show**: Focus on content fields, inline simple relationships +- **Store/Update**: Include only fields AI agents should modify + +### 2. Token Optimization + +- Remove unnecessary metadata fields +- Inline simple relationship data instead of separate API calls +- Use computed fields to provide aggregated information +- Avoid deeply nested relationship structures + +### 3. Security Considerations + +- Same authorization rules apply to MCP requests +- Use conditional fields based on AI agent permissions +- Log AI agent activities for audit purposes +- Validate AI agent inputs thoroughly + +### 4. Development Workflow + +1. Start with regular field methods +2. Identify token-heavy operations through monitoring +3. Create MCP-specific methods for optimization +4. Test both human and AI agent access patterns +5. Monitor and iterate based on usage patterns + +This MCP field system allows you to provide highly optimized data structures for AI agents while maintaining full functionality for human users, all from a single, unified codebase. \ No newline at end of file diff --git a/docs-v3/content/mcp/mcp.md b/docs-v3/content/docs/mcp/mcp.md similarity index 100% rename from docs-v3/content/mcp/mcp.md rename to docs-v3/content/docs/mcp/mcp.md diff --git a/docs-v3/content/docs/mcp/repositories.md b/docs-v3/content/docs/mcp/repositories.md new file mode 100644 index 000000000..0996a1757 --- /dev/null +++ b/docs-v3/content/docs/mcp/repositories.md @@ -0,0 +1,473 @@ +--- +title: MCP Repositories +menuTitle: Repositories +category: MCP +position: 2 +--- + +Laravel Restify repositories provide first-class support for Model Context Protocol (MCP), enabling AI agents to efficiently interact with your APIs. This page covers MCP-specific repository features that optimize token usage and provide tailored data structures for AI consumption. + +## Enabling MCP Tools + +To provide MCP server access to your repository, you must include the `HasMcpTools` trait. By default, this trait provides an index tool for AI agents: + +```php +use Binaryk\LaravelRestify\Traits\HasMcpTools; + +#[Model(Post::class)] +class PostRepository extends Repository +{ + use HasMcpTools; + + public function fields(RestifyRequest $request): array + { + return [ + field('title')->required()->searchable()->sortable(), + field('content')->string(), + field('status')->matchable(), + field('category')->matchable(), + ]; + } + + public static function related(): array + { + return [ + 'author' => BelongsTo::make('user', UserRepository::class), + 'comments' => HasMany::make('comments', CommentRepository::class), + 'tags' => BelongsToMany::make('tags', TagRepository::class), + ]; + } +} +``` + +**Generated MCP Index Tool:** + +When you include the `HasMcpTools` trait, Restify automatically generates an index tool. For example, with the post repository above that has matchable filters, the generated tool looks like this: + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "tools": [ + { + "name": "posts-index-tool", + "description": "Retrieve a paginated list of Post records from the posts repository with filtering, sorting, and search capabilities.", + "inputSchema": { + "type": "object", + "properties": { + "page": { + "type": "number", + "description": "Page number for pagination" + }, + "perPage": { + "type": "number", + "description": "Number of posts per page" + }, + "include": { + "type": "string", + "description": "Comma-separated list of relationships to include with optional field selection.\n\nAvailable relationships:\n- author (fields: name)\n- comments (fields: content)\n- tags (fields: color, name)\n\nField Selection:\nYou can specify which fields to include for each relationship using square brackets.\nSyntax: relationship[field1|field2]\n\nNested Relationships:\nYou can include deeply nested relationships using dot notation with field selection at each level.\nSyntax: relationship[fields].nested[fields].deeper[fields] - supports unlimited nesting depth\nNote: Field selection works at every nesting level independently.\n\nExamples:\n- include=author,comments (include all fields)\n- include=author[name] (selective fields with comma syntax)\n- include=author[name] (selective fields with pipe syntax)\n- include=author.posts (nested relationship - all fields)\n- include=author[name].posts[title] (nested with field selection at each level)\n- include=author[name|email].posts[title].tags[id] (deep nesting - 3 levels with field selection)\n- include=author.posts[title],author.comments[body] (multiple nested from same parent)\n- include=author[name],tags[id] (multiple relationships with field selection)\n- include=author[email|name].posts[title],tags[id] (mixing deep nested and simple relationships)" + }, + "search": { + "type": "string", + "description": "Search term to filter posts by title or description. Available searchable fields: title (e.g., search=term)" + }, + "sort": { + "type": "string", + "description": "Sorting criteria for the posts. Available options: posts.id, title (e.g., sort=field or sort=-field for descending)" + }, + "status": { + "type": "string", + "description": "Filter posts resource. Description: This is a exact match for status (e.g., status=published). It accepts negation by prefixing the column with a hyphen (e.g., -status=draft). The filter type is string." + }, + "category": { + "type": "string", + "description": "Filter posts resource. Description: This is a exact match for category (e.g., category=technology). It accepts negation by prefixing the column with a hyphen (e.g., -category=news). The filter type is string." + } + }, + "required": [] + }, + "annotations": {} + } + ] + } +} +``` + +Notice how the generated tool automatically includes: +- Pagination parameters (`page`, `perPage`) +- Relationship inclusion with field selection syntax +- Search capabilities based on searchable fields (title) +- Sort options from sortable fields (title) +- Filter parameters for each matchable field (status, category) + +## Configuring MCP Tools + +By default, the `HasMcpTools` trait only enables the **index** tool. To enable other tools like show, store, update, or delete, you need to override the corresponding methods in your repository: + +```php +use Binaryk\LaravelRestify\Traits\HasMcpTools; + +#[Model(Post::class)] +class PostRepository extends Repository +{ + use HasMcpTools; + + // Index tool is enabled by default + public function mcpAllowsIndex(): bool + { + return true; // Default: true + } + + // Enable show tool for AI agents + public function mcpAllowsShow(): bool + { + return true; // Default: false + } + + // Enable store (create) tool for AI agents + public function mcpAllowsStore(): bool + { + return true; // Default: false + } + + // Enable update tool for AI agents + public function mcpAllowsUpdate(): bool + { + return true; // Default: false + } + + // Enable delete tool for AI agents + public function mcpAllowsDelete(): bool + { + return true; // Default: false + } + + // Enable action tools for AI agents + public function mcpAllowsActions(): bool + { + return true; // Default: false + } + + // Enable getter tools for AI agents + public function mcpAllowsGetters(): bool + { + return true; // Default: false + } +} +``` + +### Conditional Tool Access + +You can conditionally enable tools based on user permissions or other criteria: + +```php +class PostRepository extends Repository +{ + use HasMcpTools; + + public function mcpAllowsShow(): bool + { + return true; // Allow all authenticated users to view posts + } + + public function mcpAllowsStore(): bool + { + // Only allow content creators to create posts via AI + return request()->user()?->hasPermissionTo('create-posts') ?? false; + } + + public function mcpAllowsUpdate(): bool + { + // Only allow editors to update posts via AI + return request()->user()?->hasRole('editor') ?? false; + } + + public function mcpAllowsDelete(): bool + { + // Only allow admins to delete posts via AI + return request()->user()?->hasRole('admin') ?? false; + } + + public function mcpAllowsActions(): bool + { + // Enable custom actions for power users + return request()->user()?->hasRole(['admin', 'editor']) ?? false; + } +} +``` + +## Tool Security + +### Default Security Approach + +Restify takes a **secure by default** approach: +- Only the **index** tool is enabled by default +- All other tools (show, store, update, delete) are **disabled** by default +- You must explicitly enable each tool you want AI agents to access +- Authorization policies still apply to all MCP requests + +### Best Practices for Tool Access + +```php +class PostRepository extends Repository +{ + use HasMcpTools; + + public function mcpAllowsShow(): bool + { + // Safe: Reading data is generally low risk + return true; + } + + public function mcpAllowsStore(): bool + { + // Moderate risk: Consider user permissions + return request()->user()?->can('create', Post::class) ?? false; + } + + public function mcpAllowsUpdate(): bool + { + // Higher risk: Require explicit permissions + return request()->user()?->hasPermissionTo('ai-edit-posts') ?? false; + } + + public function mcpAllowsDelete(): bool + { + // Highest risk: Very restrictive + return request()->user()?->hasRole('super-admin') ?? false; + } +} +``` + +## Field Optimization for AI Agents + +For detailed information about optimizing field responses for AI agents, including MCP-specific field methods, token optimization, and conditional field visibility, see the **[MCP Fields](/mcp/fields)** documentation. + + +## MCP Tool Examples + +Laravel Restify automatically generates MCP tools for AI agents based on your repository configuration. Here's what the tools look like from an AI agent's perspective: + +### Index Tool Example + +When you define a repository with fields and relationships, Restify generates an index tool: + +**Repository:** +```php +#[Model(Organization::class)] +class OrganizationRepository extends Repository +{ + public function fields(RestifyRequest $request): array + { + return [ + field('name')->searchable()->sortable(), + field('address')->matchable(), + field('city')->matchable(), + field('country')->matchable(), + ]; + } + + public static function related(): array + { + return [ + 'users' => HasMany::make('users', UserRepository::class), + 'teams' => HasMany::make('teams', TeamRepository::class), + 'tags' => BelongsToMany::make('tags', TagRepository::class), + ]; + } +} +``` + +**Generated MCP Tool:** +```json +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "tools": [ + { + "name": "organizations-index-tool", + "description": "Retrieve a paginated list of Organization records from the organizations repository with filtering, sorting, and search capabilities.", + "inputSchema": { + "type": "object", + "properties": { + "page": { + "type": "number", + "description": "Page number for pagination" + }, + "perPage": { + "type": "number", + "description": "Number of organizations per page" + }, + "include": { + "type": "string", + "description": "Comma-separated list of relationships to include with optional field selection.\n\nAvailable relationships:\n- users (fields: name)\n- teams (fields: name)\n- tags (fields: color, name)\n\nField Selection:\nYou can specify which fields to include for each relationship using square brackets.\nSyntax: relationship[field1|field2]\n\nNested Relationships:\nYou can include deeply nested relationships using dot notation with field selection at each level.\nSyntax: relationship[fields].nested[fields].deeper[fields] - supports unlimited nesting depth\nNote: Field selection works at every nesting level independently.\n\nExamples:\n- include=users,teams (include all fields)\n- include=users[name] (selective fields with comma syntax)\n- include=users[name] (selective fields with pipe syntax)\n- include=users.posts (nested relationship - all fields)\n- include=users[name].posts[title] (nested with field selection at each level)\n- include=users[name|email].posts[title].tags[id] (deep nesting - 3 levels with field selection)\n- include=users.posts[title],users.comments[body] (multiple nested from same parent)\n- include=users[name],teams[id] (multiple relationships with field selection)\n- include=users[email|name].posts[title],teams[id] (mixing deep nested and simple relationships)" + }, + "search": { + "type": "string", + "description": "Search term to filter organizations by name or description. Available searchable fields: name (e.g., search=term)" + }, + "sort": { + "type": "string", + "description": "Sorting criteria for the organizations. Available options: organizations.id, name (e.g., sort=field or sort=-field for descending)" + }, + "tags": { + "type": "string", + "description": "Filter organizations resource. This is an exact match for tags (e.g., tags=some_value). It accepts negation by prefixing the column with a hyphen (e.g., -tags=some_value). The filter type is string." + }, + "address": { + "type": "string", + "description": "Filter organizations resource. This is an exact match for address (e.g., address=some_value). It accepts negation by prefixing the column with a hyphen (e.g., -address=some_value). The filter type is string." + }, + "city": { + "type": "string", + "description": "Filter organizations resource. This is an exact match for city (e.g., city=some_value). It accepts negation by prefixing the column with a hyphen (e.g., -city=some_value). The filter type is string." + }, + "country": { + "type": "string", + "description": "Filter organizations resource. This is an exact match for country (e.g., country=some_value). It accepts negation by prefixing the column with a hyphen (e.g., -country=some_value). The filter type is string." + } + } + } + } + ] + } +} +``` + +### Show Tool Example + +**Generated Show Tool:** +```json +{ + "name": "organizations-show-tool", + "description": "Retrieve a specific Organization record by ID with optional relationship loading.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the organization to retrieve" + }, + "include": { + "type": "string", + "description": "Comma-separated list of relationships to include (e.g., include=users,teams,tags)" + } + }, + "required": ["id"] + } +} +``` + +### Store Tool Example + +**Generated Store Tool:** +```json +{ + "name": "posts-store-tool", + "description": "Create a new Post record.", + "inputSchema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title field for posts" + }, + "content": { + "type": "string", + "description": "Content field for posts" + }, + "status": { + "type": "string", + "description": "Status field for posts" + }, + "category": { + "type": "string", + "description": "Category field for posts" + } + }, + "required": [ + "title" + ] + } +} +``` + +### Update Tool Example + +**Generated Update Tool:** +```json +{ + "name": "posts-update-tool", + "description": "Update an existing Post record by ID.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the post to update" + }, + "title": { + "type": "string", + "description": "Title field for posts" + }, + "content": { + "type": "string", + "description": "Content field for posts" + }, + "status": { + "type": "string", + "description": "Status field for posts" + }, + "category": { + "type": "string", + "description": "Category field for posts" + } + }, + "required": [ + "id" + ] + } +} +``` + +### Complete Tool Set + +For a typical repository, Restify generates these MCP tools: + +- **`{resource}-index-tool`** - List/search records with pagination and filtering +- **`{resource}-show-tool`** - Get a specific record by ID +- **`{resource}-store-tool`** - Create new records +- **`{resource}-update-tool`** - Update existing records +- **`{resource}-destroy-tool`** - Delete records (if authorized) + +### Tool Features Generated from Fields + +**Field Modifiers → Tool Properties:** + +```php +field('name')->searchable() // → adds to "search" description +field('status')->matchable() // → adds "status" filter parameter +field('title')->sortable() // → adds to "sort" options +field('email')->required() // → adds to "required" array in schema +``` + +**Relationships → Include Options:** + +```php +HasMany::make('posts', PostRepository::class) // → "posts" in include options +BelongsTo::make('author', UserRepository::class) // → "author" in include options +``` + +**Validation Rules → Schema:** + +```php +field('email')->rules('email') // → type: "string", format: "email" +field('age')->rules('integer', 'min:18') // → type: "integer", minimum: 18 +field('status')->rules('in:draft,published') // → enum: ["draft", "published"] +``` + +This automatic tool generation means you define your API once in the repository, and AI agents get a complete, type-safe interface with detailed documentation and examples. + +This MCP repository system allows you to provide highly optimized APIs for AI agents while maintaining full functionality for human users, all from a single, unified codebase. diff --git a/docs-v3/content/performance/performance.md b/docs-v3/content/docs/performance/performance.md similarity index 100% rename from docs-v3/content/performance/performance.md rename to docs-v3/content/docs/performance/performance.md diff --git a/docs-v3/content/performance/solutions.md b/docs-v3/content/docs/performance/solutions.md similarity index 100% rename from docs-v3/content/performance/solutions.md rename to docs-v3/content/docs/performance/solutions.md diff --git a/docs-v3/content/quickstart.md b/docs-v3/content/docs/quickstart.md similarity index 100% rename from docs-v3/content/quickstart.md rename to docs-v3/content/docs/quickstart.md diff --git a/docs-v3/content/search/advanced-filters.md b/docs-v3/content/docs/search/advanced-filters.md similarity index 100% rename from docs-v3/content/search/advanced-filters.md rename to docs-v3/content/docs/search/advanced-filters.md diff --git a/docs-v3/content/search/basic-filters.md b/docs-v3/content/docs/search/basic-filters.md similarity index 100% rename from docs-v3/content/search/basic-filters.md rename to docs-v3/content/docs/search/basic-filters.md diff --git a/docs-v3/content/search/sorting.md b/docs-v3/content/docs/search/sorting.md similarity index 100% rename from docs-v3/content/search/sorting.md rename to docs-v3/content/docs/search/sorting.md diff --git a/docs-v3/content/testing/testing.md b/docs-v3/content/docs/testing/testing.md similarity index 100% rename from docs-v3/content/testing/testing.md rename to docs-v3/content/docs/testing/testing.md diff --git a/docs-v3/layouts/docs.vue b/docs-v3/layouts/docs.vue new file mode 100644 index 000000000..df769b1f5 --- /dev/null +++ b/docs-v3/layouts/docs.vue @@ -0,0 +1,44 @@ + + + \ No newline at end of file diff --git a/docs-v3/layouts/website.vue b/docs-v3/layouts/website.vue new file mode 100644 index 000000000..cacfed9f6 --- /dev/null +++ b/docs-v3/layouts/website.vue @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/docs-v3/pages/case-studies/index.vue b/docs-v3/pages/case-studies/index.vue new file mode 100644 index 000000000..c1ac74962 --- /dev/null +++ b/docs-v3/pages/case-studies/index.vue @@ -0,0 +1,253 @@ + + + \ No newline at end of file diff --git a/docs-v3/pages/community/index.vue b/docs-v3/pages/community/index.vue new file mode 100644 index 000000000..7ce8ad786 --- /dev/null +++ b/docs-v3/pages/community/index.vue @@ -0,0 +1,614 @@ + + + + + \ No newline at end of file diff --git a/docs-v3/pages/docs/[...slug].vue b/docs-v3/pages/docs/[...slug].vue new file mode 100644 index 000000000..b58a3c14d --- /dev/null +++ b/docs-v3/pages/docs/[...slug].vue @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/docs-v3/pages/docs/index.vue b/docs-v3/pages/docs/index.vue new file mode 100644 index 000000000..4057a8a8e --- /dev/null +++ b/docs-v3/pages/docs/index.vue @@ -0,0 +1,171 @@ + + + \ No newline at end of file diff --git a/docs-v3/pages/index.vue b/docs-v3/pages/index.vue index 1d4459420..2c3079aff 100644 --- a/docs-v3/pages/index.vue +++ b/docs-v3/pages/index.vue @@ -1,572 +1,507 @@ diff --git a/docs-v3/pages/playground/index.vue b/docs-v3/pages/playground/index.vue new file mode 100644 index 000000000..f8d6808ef --- /dev/null +++ b/docs-v3/pages/playground/index.vue @@ -0,0 +1,420 @@ + + + diff --git a/docs-v3/pages/templates/index.vue b/docs-v3/pages/templates/index.vue new file mode 100644 index 000000000..ccaa25adf --- /dev/null +++ b/docs-v3/pages/templates/index.vue @@ -0,0 +1,231 @@ + + + diff --git a/docs-v3/tailwind.config.js b/docs-v3/tailwind.config.js index b7b238725..43a8174f3 100644 --- a/docs-v3/tailwind.config.js +++ b/docs-v3/tailwind.config.js @@ -63,10 +63,10 @@ module.exports = { fontWeight: '600' }, code: { - color: theme('colors.gray.800'), - backgroundColor: theme('colors.gray.100'), - padding: '0.25rem 0.375rem', - borderRadius: '0.25rem', + color: theme('colors.blue.900'), + backgroundColor: theme('colors.blue.200'), + padding: '0.375rem 0.5rem', + borderRadius: '0.375rem', fontSize: '0.875em', fontWeight: '500' }, @@ -101,8 +101,8 @@ module.exports = { color: theme('colors.gray.100') }, code: { - color: theme('colors.gray.200'), - backgroundColor: theme('colors.gray.800') + color: theme('colors.blue.300'), + backgroundColor: theme('colors.gray.700') }, pre: { backgroundColor: theme('colors.gray.800')