diff --git a/.claude/skills/cashier-stripe-development/SKILL.md b/.claude/skills/cashier-stripe-development/SKILL.md
index 355e8a92..3611b62e 100644
--- a/.claude/skills/cashier-stripe-development/SKILL.md
+++ b/.claude/skills/cashier-stripe-development/SKILL.md
@@ -33,9 +33,9 @@ ## Basic Usage
### Installation
```bash
-php artisan vendor:publish --tag="cashier-migrations"
-php artisan migrate
-php artisan vendor:publish --tag="cashier-config"
+vendor/bin/sail artisan vendor:publish --tag="cashier-migrations"
+vendor/bin/sail artisan migrate
+vendor/bin/sail artisan vendor:publish --tag="cashier-config"
```
### Environment Variables
diff --git a/.claude/skills/configuring-horizon/SKILL.md b/.claude/skills/configuring-horizon/SKILL.md
index bed1e74c..112c0d13 100644
--- a/.claude/skills/configuring-horizon/SKILL.md
+++ b/.claude/skills/configuring-horizon/SKILL.md
@@ -24,7 +24,7 @@ ## Basic Usage
### Installation
```bash
-php artisan horizon:install
+vendor/bin/sail artisan horizon:install
```
### Supervisor Configuration
@@ -70,7 +70,7 @@ ### Dashboard Authorization
## Verification
-1. Run `php artisan horizon` and visit `/horizon`
+1. Run `vendor/bin/sail artisan horizon` and visit `/horizon`
2. Confirm dashboard access is restricted as expected
3. Check that metrics populate after scheduling `horizon:snapshot`
@@ -81,5 +81,5 @@ ## Common Pitfalls
- Always check `config/horizon.php` before making changes to understand the current supervisor and environment configuration.
- The `environments` array overrides only the keys you specify. It merges into `defaults` and does not replace it.
- The timeout chain must be ordered: job `timeout` less than supervisor `timeout` less than `retry_after`. The wrong order can cause jobs to be retried before Horizon finishes timing them out.
-- The metrics dashboard stays blank until `horizon:snapshot` is scheduled. Running `php artisan horizon` alone does not populate metrics.
+- The metrics dashboard stays blank until `horizon:snapshot` is scheduled. Running `vendor/bin/sail artisan horizon` alone does not populate metrics.
- Always use `search-docs` for the latest Horizon documentation rather than relying on this skill alone.
\ No newline at end of file
diff --git a/.claude/skills/inertia-vue-development/SKILL.md b/.claude/skills/inertia-vue-development/SKILL.md
index a0612246..c69fd98e 100644
--- a/.claude/skills/inertia-vue-development/SKILL.md
+++ b/.claude/skills/inertia-vue-development/SKILL.md
@@ -1,6 +1,6 @@
---
name: inertia-vue-development
-description: "Develops Inertia.js v2 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using ,
` without preventing default submission (use `
` component or `@submit.prevent`)
-- Forgetting to check if `
` component is available in your Inertia version
\ No newline at end of file
+- Forgetting to check if `
` component is available in your Inertia version
+- Using `router.cancel()` instead of `router.cancelAll()` (v3 breaking change)
+- Using `router.on('invalid', ...)` or `router.on('exception', ...)` instead of the renamed `httpException` and `networkError` events
\ No newline at end of file
diff --git a/.claude/skills/mcp-development/SKILL.md b/.claude/skills/mcp-development/SKILL.md
index 71269844..fb38e649 100644
--- a/.claude/skills/mcp-development/SKILL.md
+++ b/.claude/skills/mcp-development/SKILL.md
@@ -8,148 +8,88 @@
# MCP Development
-## Documentation First
+## Documentation
-**CRITICAL**: Always use `search-docs` BEFORE writing MCP code. The documentation is version-specific, comprehensive, and always up-to-date.
+Use `search-docs` for detailed Laravel MCP patterns and documentation.
-
-```bash
+## Basic Usage
-# Example searches
+Register MCP servers in `routes/ai.php`:
-search-docs(['mcp tools', 'mcp resources', 'mcp validation'])
-```
-
-## Quick Reference
-
-### Artisan Commands
-
-Create MCP Primitives"
-```bash
-php artisan make:mcp-tool ToolName
-php artisan make:mcp-resource ResourceName
-php artisan make:mcp-prompt PromptName
-php artisan make:mcp-server ServerName
-```
-
-### Basic Tool Implementation
-
-
+
+```php
+use Laravel\Mcp\Facades\Mcp;
+
+Mcp::web();
+```
+
+### Creating MCP Primitives
+
+Create MCP tools, resources, prompts, and servers using artisan commands:
+
+```bash
+vendor/bin/sail artisan make:mcp-tool ToolName # Create a tool
+
+vendor/bin/sail artisan make:mcp-resource ResourceName # Create a resource
+
+vendor/bin/sail artisan make:mcp-prompt PromptName # Create a prompt
+
+vendor/bin/sail artisan make:mcp-server ServerName # Create a server
+
+```
+
+After creating primitives, register them in your server's `$tools`, `$resources`, or `$prompts` properties.
+
+### Tools
+
+
```php
-use Illuminate\Contracts\JsonSchema\JsonSchema;
-use Laravel\Mcp\Request;
-use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;
+use Laravel\Mcp\Server\Request;
+use Laravel\Mcp\Server\Response;
class MyTool extends Tool
{
- protected string $description = 'Tool description for LLM';
-
- public function schema(JsonSchema $schema): array
- {
- return [
- 'param' => $schema->string()->required(),
- ];
- }
-
public function handle(Request $request): Response
{
- return Response::text($request->get('param'));
+ return new Response(['result' => 'success']);
}
}
```
-### Basic Resource Implementation
+### Registering Primitives in a Server
-
+Each MCP server must explicitly declare the tools, resources, and prompts it exposes.
+
+
```php
-use Laravel\Mcp\Response;
-use Laravel\Mcp\Server\Resource;
+use Laravel\Mcp\Server;
-class MyResource extends Resource
+class AppServer extends Server
{
- protected string $description = 'Resource description';
- protected string $uri = 'file://path/to/resource';
- protected string $mimeType = 'text/markdown';
+ protected array $tools = [
+ \App\Mcp\Tools\MyTool::class,
+ ];
- public function handle(): Response
- {
- return Response::text($content);
- }
+ protected array $resources = [
+ \App\Mcp\Resources\MyResource::class,
+ ];
+
+ protected array $prompts = [
+ \App\Mcp\Prompts\MyPrompt::class,
+ ];
}
```
-### Response Methods
+## Verification
-
-```php
-Response::text('Text content');
-Response::error('Error message');
-Response::structured(['key' => 'value']);
-```
-
-## Testing MCP Primitives
-
-Test tools, resources, and prompts directly on their server:
-
-
-```php
-// Test a tool
-$response = MyServer::tool(MyTool::class, ['param' => 'value']);
-$response->assertOk()->assertSee('Expected text');
-
-// Test as authenticated user
-$response = MyServer::actingAs($user)->tool(MyTool::class, [...]);
-
-// Available assertions
-$response->assertOk();
-$response->assertSee('text');
-$response->assertHasErrors();
-$response->assertHasNoErrors();
-$response->assertName('tool-name');
-$response->assertSentNotification('event/type', ['data' => 'value']);
-```
-
-### MCP Inspector
-
-Test interactively using the inspector:
-
-
-```bash
-php artisan mcp:inspector mcp/my-server # Web server
-
-php artisan mcp:inspector my-server # Local server
-
-```
-
-## Available Features
-
-The following features exist—**use `search-docs` for implementation details**:
-
-- **Tools**: `schema()`, validation, annotations (`#[IsReadOnly]`, `#[IsDestructive]`, etc.)
-- **Resources**: URI templates (`HasUriTemplate`), Dynamic resources
-- **Prompts**: Arguments, multi-message responses
-- **All primitives**: Dependency injection, `shouldRegister()`, validation
-- **Responses**: Text, error, structured, streaming, metadata
-- **Server registration**: Web routes, local routes, OAuth
-
-## Critical Imports
-
-
-```php
-use Laravel\Mcp\Request; // NOT Laravel\Mcp\Server\Request
-use Laravel\Mcp\Response; // NOT Laravel\Mcp\Server\Response
-use Laravel\Mcp\Server\Tool;
-use Laravel\Mcp\Server\Resource;
-use Laravel\Mcp\Server\Prompt;
-use Illuminate\Contracts\JsonSchema\JsonSchema;
-```
+1. Check `routes/ai.php` for proper registration
+2. Test tool via MCP client
## Common Pitfalls
-- **Not using `search-docs` before implementation**
-- Wrong imports: `Laravel\Mcp\Server\Request` (wrong) vs `Laravel\Mcp\Request` (correct)
-- Forgetting `schema()` method for tools with parameters
-- Missing required properties: `$description`, `$uri`, `$mimeType`
-- Wrong response pattern: `new Response()` instead of `Response::text()`
-- Running `mcp:start` command locally (hangs waiting for stdin)
\ No newline at end of file
+- Running `mcp:start` command (it hangs waiting for input)
+- Using HTTPS locally with Node-based MCP clients
+- Not using `search-docs` for the latest MCP documentation
+- Not registering MCP server routes in `routes/ai.php`
+- Do not register `ai.php` in `bootstrap.php`; it is registered automatically.
\ No newline at end of file
diff --git a/.claude/skills/pest-testing/SKILL.md b/.claude/skills/pest-testing/SKILL.md
index ba774e71..dc6f5d17 100644
--- a/.claude/skills/pest-testing/SKILL.md
+++ b/.claude/skills/pest-testing/SKILL.md
@@ -16,7 +16,7 @@ ## Basic Usage
### Creating Tests
-All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
+All tests must be written using Pest. Use `vendor/bin/sail artisan make:test --pest {name}`.
### Test Organization
@@ -35,9 +35,9 @@ ### Basic Test Structure
### Running Tests
-- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
-- Run all tests: `php artisan test --compact`.
-- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
+- Run minimal tests with filter before finalizing: `vendor/bin/sail artisan test --compact --filter=testName`.
+- Run all tests: `vendor/bin/sail artisan test --compact`.
+- Run file: `vendor/bin/sail artisan test --compact tests/Feature/ExampleTest.php`.
## Assertions
diff --git a/.claude/skills/wayfinder-development/SKILL.md b/.claude/skills/wayfinder-development/SKILL.md
index 0b306459..451995db 100644
--- a/.claude/skills/wayfinder-development/SKILL.md
+++ b/.claude/skills/wayfinder-development/SKILL.md
@@ -18,11 +18,11 @@ ### Generate Routes
Run after route changes if Vite plugin isn't installed:
```bash
-php artisan wayfinder:generate --no-interaction
+vendor/bin/sail artisan wayfinder:generate --no-interaction
```
For form helpers, use `--with-form` flag:
```bash
-php artisan wayfinder:generate --with-form --no-interaction
+vendor/bin/sail artisan wayfinder:generate --with-form --no-interaction
```
### Import Patterns
@@ -69,7 +69,7 @@ ## Wayfinder + Inertia
## Verification
-1. Run `php artisan wayfinder:generate` to regenerate routes if Vite plugin isn't installed
+1. Run `vendor/bin/sail artisan wayfinder:generate` to regenerate routes if Vite plugin isn't installed
2. Check TypeScript imports resolve correctly
3. Verify route URLs match expected paths
diff --git a/CLAUDE.md b/CLAUDE.md
index e8113c85..50ebbb7e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -12,24 +12,23 @@ ## Foundational Context
- php - 8.4
- inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3
- laravel/ai (AI) - v0
+- laravel/boost (BOOST) - v2
- laravel/cashier (CASHIER) - v16
- laravel/framework (LARAVEL) - v13
- laravel/horizon (HORIZON) - v5
- laravel/mcp (MCP) - v0
- laravel/nightwatch (NIGHTWATCH) - v1
-- laravel/pennant (PENNANT) - v1
- laravel/prompts (PROMPTS) - v0
- laravel/reverb (REVERB) - v1
- laravel/socialite (SOCIALITE) - v5
- laravel/wayfinder (WAYFINDER) - v0
-- laravel/boost (BOOST) - v2
- laravel/pail (PAIL) - v1
- laravel/pint (PINT) - v1
- laravel/sail (SAIL) - v1
- laravel/telescope (TELESCOPE) - v5
- pestphp/pest (PEST) - v4
- phpunit/phpunit (PHPUNIT) - v12
-- @inertiajs/vue3 (INERTIA_VUE) - v2
+- @inertiajs/vue3 (INERTIA_VUE) - v3
- tailwindcss (TAILWINDCSS) - v4
- vue (VUE) - v3
- @laravel/echo-vue (ECHO_VUE) - v2
@@ -45,15 +44,12 @@ ## Skills Activation
- `cashier-stripe-development` — Handles Laravel Cashier Stripe integration including subscriptions, webhooks, Stripe Checkout, invoices, charges, refunds, trials, coupons, metered billing, and payment failure handling. Triggered when a user mentions Cashier, Billable, IncompletePayment, stripe_id, newSubscription, Stripe subscriptions, or billing. Also applies when setting up webhooks, handling SCA/3DS payment failures, testing with Stripe test cards, or troubleshooting incomplete subscriptions, CSRF webhook errors, or migration publish issues.
- `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns.
- `configuring-horizon` — Use this skill whenever the user mentions Horizon by name in a Laravel context. Covers the full Horizon lifecycle: installing Horizon (horizon:install, Sail setup), configuring config/horizon.php (supervisor blocks, queue assignments, balancing strategies, minProcesses/maxProcesses), fixing the dashboard (authorization via Gate::define viewHorizon, blank metrics, horizon:snapshot scheduling), and troubleshooting production issues (worker crashes, timeout chain ordering, LongWaitDetected notifications, waits config). Also covers job tagging and silencing. Do not use for generic Laravel queues without Horizon, SQS or database drivers, standalone Redis setup, Linux supervisord, Telescope, or job batching.
-- `mcp-development` — Use this skill for Laravel MCP development only. Trigger when creating or editing MCP tools, resources, prompts, or servers in Laravel projects. Covers: artisan make:mcp-\* generators, mcp:inspector, routes/ai.php, Tool/Resource/Prompt classes, schema validation, shouldRegister(), OAuth setup, URI templates, read-only attributes, and MCP debugging. Do not use for non-Laravel MCP projects or generic AI features without MCP.
-- `pennant-development` — Use when working with Laravel Pennant the official Laravel feature flag package. Trigger whenever the query mentions Pennant by name or involves feature flags or feature toggles in a Laravel project. Tasks include defining feature flags checking whether features are active creating class based features in `app/Features` using Blade `@feature` directives scoping flags to users or teams building custom Pennant storage drivers protecting routes with feature flags testing feature flags with Pest or PHPUnit and implementing A B testing or gradual rollouts with feature flags. Do not trigger for generic Laravel configuration authorization policies authentication or non Pennant feature management systems.
+- `mcp-development` — Use this skill for Laravel MCP development only. Trigger when creating or editing MCP tools, resources, prompts, or servers in Laravel projects. Covers: artisan make:mcp-* generators, mcp:inspector, routes/ai.php, Tool/Resource/Prompt classes, schema validation, shouldRegister(), OAuth setup, URI templates, read-only attributes, and MCP debugging. Do not use for non-Laravel MCP projects or generic AI features without MCP.
- `socialite-development` — Manages OAuth social authentication with Laravel Socialite. Activate when adding social login providers; configuring OAuth redirect/callback flows; retrieving authenticated user details; customizing scopes or parameters; setting up community providers; testing with Socialite fakes; or when the user mentions social login, OAuth, Socialite, or third-party authentication.
- `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task
- `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code.
-- `inertia-vue-development` — Develops Inertia.js v2 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using ,
, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation.
+- `inertia-vue-development` — Develops Inertia.js v3 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using ,
, useForm, useHttp, setLayoutProps, or router; working with deferred props, prefetching, optimistic updates, instant visits, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation.
- `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS.
-- `ai-sdk-development` — Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter).
-- `medialibrary-development` — Build and work with spatie/laravel-medialibrary features including associating files with Eloquent models, defining media collections and conversions, generating responsive images, and retrieving media URLs and paths.
## Conventions
@@ -72,7 +68,7 @@ ## Application Structure & Architecture
## Frontend Bundling
-- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
+- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `vendor/bin/sail npm run build`, `vendor/bin/sail npm run dev`, or `vendor/bin/sail composer run dev`. Ask them.
## Documentation Files
@@ -110,22 +106,21 @@ ### Search Syntax
## Artisan
-- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
-- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
-- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
+- Run Artisan commands directly via the command line (e.g., `vendor/bin/sail artisan route:list`). Use `vendor/bin/sail artisan list` to discover available commands and `vendor/bin/sail artisan [command] --help` to check parameters.
+- Inspect routes with `vendor/bin/sail artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
+- Read configuration values using dot notation: `vendor/bin/sail artisan config:show app.name`, `vendor/bin/sail artisan config:show database.default`. Or read config files directly from the `config/` directory.
- To check environment variables, read the `.env` file directly.
## Tinker
- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code.
-- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'`
- - Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'`
+- Always use single quotes to prevent shell expansion: `vendor/bin/sail artisan tinker --execute 'Your::code();'`
+ - Double quotes for PHP strings inside: `vendor/bin/sail artisan tinker --execute 'User::where("active", true)->count();'`
=== php rules ===
# PHP
-- Always declare `declare(strict_types=1);` at the top of every `.php` file.
- Always use curly braces for control structures, even for single-line bodies.
- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private.
- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool`
@@ -133,19 +128,26 @@ # PHP
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
- Use array shape type definitions in PHPDoc blocks.
-=== herd rules ===
+=== sail rules ===
-# Laravel Herd
+# Laravel Sail
-- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available.
-- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start `, `herd php:list`). Run `herd list` to discover all available commands.
+- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
+- Start services using `vendor/bin/sail up -d` and stop them with `vendor/bin/sail stop`.
+- Open the application in the browser by running `vendor/bin/sail open`.
+- Always prefix PHP, Artisan, Composer, and Node commands with `vendor/bin/sail`. Examples:
+ - Run Artisan Commands: `vendor/bin/sail artisan migrate`
+ - Install Composer packages: `vendor/bin/sail composer install`
+ - Execute Node commands: `vendor/bin/sail npm run dev`
+ - Execute PHP scripts: `vendor/bin/sail php [script]`
+- View all available Sail commands by running `vendor/bin/sail` without arguments.
=== tests rules ===
# Test Enforcement
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
-- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
+- Run the minimum number of tests needed to ensure code quality and speed. Use `vendor/bin/sail artisan test --compact` with a specific filename or filter.
=== inertia-laravel/core rules ===
@@ -174,13 +176,13 @@ # Inertia v3
# Do Things the Laravel Way
-- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
-- If you're creating a generic PHP class, use `php artisan make:class`.
+- Use `vendor/bin/sail artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `vendor/bin/sail artisan list` and check their parameters with `vendor/bin/sail artisan [command] --help`.
+- If you're creating a generic PHP class, use `vendor/bin/sail artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
### Model Creation
-- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
+- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `vendor/bin/sail artisan make:model --help` to check the available options.
## APIs & Eloquent Resources
@@ -194,11 +196,11 @@ ## Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
-- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
+- When creating tests, make use of `vendor/bin/sail artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
## Vite Error
-- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
+- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `vendor/bin/sail npm run build` or ask the user to run `vendor/bin/sail npm run dev` or `vendor/bin/sail composer run dev`.
=== wayfinder/core rules ===
@@ -210,15 +212,15 @@ # Laravel Wayfinder
# Laravel Pint Code Formatter
-- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
-- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
+- If you have modified any PHP files, you must run `vendor/bin/sail bin pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
+- Do not run `vendor/bin/sail bin pint --test --format agent`, simply run `vendor/bin/sail bin pint --format agent` to fix any formatting issues.
=== pest/core rules ===
## Pest
-- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
-- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
+- This project uses Pest for testing. Create tests: `vendor/bin/sail artisan make:test --pest {name}`.
+- Run tests: `vendor/bin/sail artisan test --compact` or filter: `vendor/bin/sail artisan test --compact --filter=testName`.
- Do NOT delete tests without approval.
=== inertia-vue/core rules ===
@@ -226,23 +228,8 @@ ## Pest
# Inertia + Vue
Vue components must have a single root element.
-
- IMPORTANT: Activate `inertia-vue-development` when working with Inertia Vue client-side patterns.
-=== laravel/ai rules ===
-
-## Laravel AI SDK
-
-- This application uses the Laravel AI SDK (`laravel/ai`) for all AI functionality.
-- Activate the `developing-with-ai-sdk` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter).
-
-=== spatie/laravel-medialibrary rules ===
-
-## Media Library
-
-- `spatie/laravel-medialibrary` associates files with Eloquent models, with support for collections, conversions, and responsive images.
-- Always activate the `medialibrary-development` skill when working with media uploads, conversions, collections, responsive images, or any code that uses the `HasMedia` interface or `InteractsWithMedia` trait.
-
# Project-Specific Rules
diff --git a/app/Http/Middleware/Mcp/AuthenticateMcpToken.php b/app/Http/Middleware/Mcp/AuthenticateMcpToken.php
new file mode 100644
index 00000000..937d9bea
--- /dev/null
+++ b/app/Http/Middleware/Mcp/AuthenticateMcpToken.php
@@ -0,0 +1,53 @@
+bearerToken();
+
+ if (! $token) {
+ return response()->json(['message' => 'Missing API key.'], Response::HTTP_UNAUTHORIZED);
+ }
+
+ if (! str_starts_with($token, 'tp_') || strlen($token) !== 51) {
+ return response()->json(['message' => 'Invalid API key.'], Response::HTTP_UNAUTHORIZED);
+ }
+
+ $lookup = substr($token, 3, 16);
+ $apiToken = ApiToken::where('token_lookup', $lookup)->first();
+
+ if (! $apiToken || ! Hash::check($token, $apiToken->token_hash)) {
+ return response()->json(['message' => 'Invalid API key.'], Response::HTTP_UNAUTHORIZED);
+ }
+
+ if ($apiToken->status === 'expired') {
+ return response()->json(['message' => 'API key has expired.'], Response::HTTP_UNAUTHORIZED);
+ }
+
+ $apiToken->update(['last_used_at' => now()]);
+
+ $workspace = $apiToken->workspace;
+ $user = $workspace->owner;
+
+ if (! $user) {
+ return response()->json(['message' => 'No workspace owner found.'], Response::HTTP_UNAUTHORIZED);
+ }
+
+ $user->current_workspace_id = $workspace->id;
+ Auth::login($user);
+
+ return $next($request);
+ }
+}
diff --git a/app/Mcp/Servers/TryPostServer.php b/app/Mcp/Servers/TryPostServer.php
new file mode 100644
index 00000000..f0d2279c
--- /dev/null
+++ b/app/Mcp/Servers/TryPostServer.php
@@ -0,0 +1,66 @@
+validate([
+ 'name' => ['required', 'string', 'max:255'],
+ 'expires_at' => ['nullable', 'date', 'after:now'],
+ ]);
+
+ $result = CreateApiKey::execute($request->user()->currentWorkspace, $validated);
+
+ return Response::structured([
+ ...$result['token']->toArray(),
+ 'token' => $result['plain_token'],
+ ]);
+ }
+
+ public function schema(JsonSchema $schema): array
+ {
+ return [
+ 'name' => $schema->string()->required()->description('The API key name.'),
+ 'expires_at' => $schema->string()->description('Optional expiration date.'),
+ ];
+ }
+}
diff --git a/app/Mcp/Tools/ApiKey/DeleteApiKeyTool.php b/app/Mcp/Tools/ApiKey/DeleteApiKeyTool.php
new file mode 100644
index 00000000..58f4d062
--- /dev/null
+++ b/app/Mcp/Tools/ApiKey/DeleteApiKeyTool.php
@@ -0,0 +1,35 @@
+user()->current_workspace_id)
+ ->findOrFail(data_get($request->validated(), 'api_key_id'));
+
+ DeleteApiKey::execute($apiToken);
+
+ return Response::structured(['deleted' => true]);
+ }
+
+ public function schema(JsonSchema $schema): array
+ {
+ return [
+ 'api_key_id' => $schema->string()->required()->description('The API key ID to delete.'),
+ ];
+ }
+}
diff --git a/app/Mcp/Tools/ApiKey/ListApiKeysTool.php b/app/Mcp/Tools/ApiKey/ListApiKeysTool.php
new file mode 100644
index 00000000..6ff34d85
--- /dev/null
+++ b/app/Mcp/Tools/ApiKey/ListApiKeysTool.php
@@ -0,0 +1,24 @@
+user()->currentWorkspace->apiTokens()->latest()->get();
+
+ return Response::structured($tokens->toArray());
+ }
+}
diff --git a/app/Mcp/Tools/Hashtag/CreateHashtagTool.php b/app/Mcp/Tools/Hashtag/CreateHashtagTool.php
new file mode 100644
index 00000000..1aa36ba4
--- /dev/null
+++ b/app/Mcp/Tools/Hashtag/CreateHashtagTool.php
@@ -0,0 +1,37 @@
+validate([
+ 'name' => ['required', 'string', 'max:255'],
+ 'hashtags' => ['required', 'string'],
+ ]);
+
+ $hashtag = CreateHashtag::execute($request->user()->currentWorkspace, $validated);
+
+ return Response::structured($hashtag->toArray());
+ }
+
+ public function schema(JsonSchema $schema): array
+ {
+ return [
+ 'name' => $schema->string()->required()->description('The hashtag group name.'),
+ 'hashtags' => $schema->string()->required()->description('The hashtags string (e.g. "#tech #ai #startup").'),
+ ];
+ }
+}
diff --git a/app/Mcp/Tools/Hashtag/DeleteHashtagTool.php b/app/Mcp/Tools/Hashtag/DeleteHashtagTool.php
new file mode 100644
index 00000000..5df1d979
--- /dev/null
+++ b/app/Mcp/Tools/Hashtag/DeleteHashtagTool.php
@@ -0,0 +1,35 @@
+user()->current_workspace_id)
+ ->findOrFail(data_get($request->validated(), 'hashtag_id'));
+
+ DeleteHashtag::execute($hashtag);
+
+ return Response::structured(['deleted' => true]);
+ }
+
+ public function schema(JsonSchema $schema): array
+ {
+ return [
+ 'hashtag_id' => $schema->string()->required()->description('The hashtag group ID to delete.'),
+ ];
+ }
+}
diff --git a/app/Mcp/Tools/Hashtag/ListHashtagsTool.php b/app/Mcp/Tools/Hashtag/ListHashtagsTool.php
new file mode 100644
index 00000000..2a830593
--- /dev/null
+++ b/app/Mcp/Tools/Hashtag/ListHashtagsTool.php
@@ -0,0 +1,24 @@
+user()->currentWorkspace->hashtags()->latest()->get();
+
+ return Response::structured($hashtags->toArray());
+ }
+}
diff --git a/app/Mcp/Tools/Hashtag/UpdateHashtagTool.php b/app/Mcp/Tools/Hashtag/UpdateHashtagTool.php
new file mode 100644
index 00000000..0bbc1aa2
--- /dev/null
+++ b/app/Mcp/Tools/Hashtag/UpdateHashtagTool.php
@@ -0,0 +1,43 @@
+validate([
+ 'hashtag_id' => ['required', 'string'],
+ 'name' => ['required', 'string', 'max:255'],
+ 'hashtags' => ['required', 'string'],
+ ]);
+
+ $hashtag = WorkspaceHashtag::where('workspace_id', $request->user()->current_workspace_id)
+ ->findOrFail(data_get($validated, 'hashtag_id'));
+
+ $hashtag = UpdateHashtag::execute($hashtag, $validated);
+
+ return Response::structured($hashtag->toArray());
+ }
+
+ public function schema(JsonSchema $schema): array
+ {
+ return [
+ 'hashtag_id' => $schema->string()->required()->description('The hashtag group ID.'),
+ 'name' => $schema->string()->required()->description('The new name.'),
+ 'hashtags' => $schema->string()->required()->description('The new hashtags string.'),
+ ];
+ }
+}
diff --git a/app/Mcp/Tools/Label/CreateLabelTool.php b/app/Mcp/Tools/Label/CreateLabelTool.php
new file mode 100644
index 00000000..05d42828
--- /dev/null
+++ b/app/Mcp/Tools/Label/CreateLabelTool.php
@@ -0,0 +1,37 @@
+validate([
+ 'name' => ['required', 'string', 'max:255'],
+ 'color' => ['required', 'string', 'max:7', 'regex:/^#[0-9A-Fa-f]{6}$/'],
+ ]);
+
+ $label = CreateLabel::execute($request->user()->currentWorkspace, $validated);
+
+ return Response::structured($label->toArray());
+ }
+
+ public function schema(JsonSchema $schema): array
+ {
+ return [
+ 'name' => $schema->string()->required()->description('The label name.'),
+ 'color' => $schema->string()->required()->description('Hex color code (e.g. #FF5733).'),
+ ];
+ }
+}
diff --git a/app/Mcp/Tools/Label/DeleteLabelTool.php b/app/Mcp/Tools/Label/DeleteLabelTool.php
new file mode 100644
index 00000000..29f42511
--- /dev/null
+++ b/app/Mcp/Tools/Label/DeleteLabelTool.php
@@ -0,0 +1,35 @@
+user()->current_workspace_id)
+ ->findOrFail(data_get($request->validated(), 'label_id'));
+
+ DeleteLabel::execute($label);
+
+ return Response::structured(['deleted' => true]);
+ }
+
+ public function schema(JsonSchema $schema): array
+ {
+ return [
+ 'label_id' => $schema->string()->required()->description('The label ID to delete.'),
+ ];
+ }
+}
diff --git a/app/Mcp/Tools/Label/ListLabelsTool.php b/app/Mcp/Tools/Label/ListLabelsTool.php
new file mode 100644
index 00000000..f0ccb8cc
--- /dev/null
+++ b/app/Mcp/Tools/Label/ListLabelsTool.php
@@ -0,0 +1,24 @@
+user()->currentWorkspace->labels()->latest()->get();
+
+ return Response::structured($labels->toArray());
+ }
+}
diff --git a/app/Mcp/Tools/Label/UpdateLabelTool.php b/app/Mcp/Tools/Label/UpdateLabelTool.php
new file mode 100644
index 00000000..f2be4ff4
--- /dev/null
+++ b/app/Mcp/Tools/Label/UpdateLabelTool.php
@@ -0,0 +1,43 @@
+validate([
+ 'label_id' => ['required', 'string'],
+ 'name' => ['required', 'string', 'max:255'],
+ 'color' => ['required', 'string', 'max:7', 'regex:/^#[0-9A-Fa-f]{6}$/'],
+ ]);
+
+ $label = WorkspaceLabel::where('workspace_id', $request->user()->current_workspace_id)
+ ->findOrFail(data_get($validated, 'label_id'));
+
+ $label = UpdateLabel::execute($label, $validated);
+
+ return Response::structured($label->toArray());
+ }
+
+ public function schema(JsonSchema $schema): array
+ {
+ return [
+ 'label_id' => $schema->string()->required()->description('The label ID.'),
+ 'name' => $schema->string()->required()->description('The new name.'),
+ 'color' => $schema->string()->required()->description('Hex color code (e.g. #FF5733).'),
+ ];
+ }
+}
diff --git a/app/Mcp/Tools/Post/CreatePostTool.php b/app/Mcp/Tools/Post/CreatePostTool.php
new file mode 100644
index 00000000..9261ae7a
--- /dev/null
+++ b/app/Mcp/Tools/Post/CreatePostTool.php
@@ -0,0 +1,33 @@
+user()->currentWorkspace;
+ $post = CreatePost::execute($workspace, $request->user(), $request->validated());
+ $post->load(['postPlatforms.socialAccount']);
+
+ return Response::structured($post->toArray());
+ }
+
+ public function schema(JsonSchema $schema): array
+ {
+ return [
+ 'date' => $schema->string()->description('The scheduled date (Y-m-d). Defaults to today.'),
+ ];
+ }
+}
diff --git a/app/Mcp/Tools/Post/DeletePostTool.php b/app/Mcp/Tools/Post/DeletePostTool.php
new file mode 100644
index 00000000..923b7e2a
--- /dev/null
+++ b/app/Mcp/Tools/Post/DeletePostTool.php
@@ -0,0 +1,35 @@
+user()->current_workspace_id)
+ ->findOrFail(data_get($request->validated(), 'post_id'));
+
+ DeletePost::execute($post);
+
+ return Response::structured(['deleted' => true]);
+ }
+
+ public function schema(JsonSchema $schema): array
+ {
+ return [
+ 'post_id' => $schema->string()->required()->description('The post ID to delete.'),
+ ];
+ }
+}
diff --git a/app/Mcp/Tools/Post/GetPostTool.php b/app/Mcp/Tools/Post/GetPostTool.php
new file mode 100644
index 00000000..5868b04d
--- /dev/null
+++ b/app/Mcp/Tools/Post/GetPostTool.php
@@ -0,0 +1,35 @@
+user()->current_workspace_id)
+ ->with(['postPlatforms.socialAccount', 'postPlatforms.media', 'labels'])
+ ->findOrFail(data_get($request->validated(), 'post_id'));
+
+ return Response::structured($post->toArray());
+ }
+
+ public function schema(JsonSchema $schema): array
+ {
+ return [
+ 'post_id' => $schema->string()->required()->description('The post ID to retrieve.'),
+ ];
+ }
+}
diff --git a/app/Mcp/Tools/Post/ListPostsTool.php b/app/Mcp/Tools/Post/ListPostsTool.php
new file mode 100644
index 00000000..fa4a9f3b
--- /dev/null
+++ b/app/Mcp/Tools/Post/ListPostsTool.php
@@ -0,0 +1,28 @@
+user()->currentWorkspace
+ ->posts()
+ ->with(['postPlatforms.socialAccount', 'labels'])
+ ->latest('scheduled_at')
+ ->paginate(50);
+
+ return Response::structured($posts->toArray());
+ }
+}
diff --git a/app/Mcp/Tools/Workspace/GetWorkspaceTool.php b/app/Mcp/Tools/Workspace/GetWorkspaceTool.php
new file mode 100644
index 00000000..994d5b67
--- /dev/null
+++ b/app/Mcp/Tools/Workspace/GetWorkspaceTool.php
@@ -0,0 +1,22 @@
+user()->currentWorkspace->toArray());
+ }
+}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 7ba8431c..1fde4800 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -21,6 +21,7 @@
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Cache\RateLimiting\Limit;
+use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -49,7 +50,10 @@ class AppServiceProvider extends ServiceProvider
*/
public function register(): void
{
- //
+ if ($this->app->environment('local') && class_exists(\Laravel\Telescope\TelescopeServiceProvider::class)) {
+ $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class);
+ $this->app->register(TelescopeServiceProvider::class);
+ }
}
/**
@@ -123,6 +127,7 @@ protected function configureDefaults(): void
// Disable wrapping of JSON resources
JsonResource::withoutWrapping();
+ Model::shouldBeStrict(! $this->app->isProduction());
DB::prohibitDestructiveCommands(
app()->isProduction(),
diff --git a/app/Providers/TelescopeServiceProvider.php b/app/Providers/TelescopeServiceProvider.php
new file mode 100644
index 00000000..4de2a6b2
--- /dev/null
+++ b/app/Providers/TelescopeServiceProvider.php
@@ -0,0 +1,65 @@
+hideSensitiveRequestDetails();
+
+ $isLocal = $this->app->environment('local');
+
+ Telescope::filter(function (IncomingEntry $entry) use ($isLocal) {
+ return $isLocal ||
+ $entry->isReportableException() ||
+ $entry->isFailedRequest() ||
+ $entry->isFailedJob() ||
+ $entry->isScheduledTask() ||
+ $entry->hasMonitoredTag();
+ });
+ }
+
+ /**
+ * Prevent sensitive request details from being logged by Telescope.
+ */
+ protected function hideSensitiveRequestDetails(): void
+ {
+ if ($this->app->environment('local')) {
+ return;
+ }
+
+ Telescope::hideRequestParameters(['_token']);
+
+ Telescope::hideRequestHeaders([
+ 'cookie',
+ 'x-csrf-token',
+ 'x-xsrf-token',
+ ]);
+ }
+
+ /**
+ * Register the Telescope gate.
+ *
+ * This gate determines who can access Telescope in non-local environments.
+ */
+ protected function gate(): void
+ {
+ Gate::define('viewTelescope', function (User $user) {
+ return in_array($user->email, [
+ //
+ ]);
+ });
+ }
+}
diff --git a/boost.json b/boost.json
index 86a3c4ef..a48dd87f 100644
--- a/boost.json
+++ b/boost.json
@@ -10,6 +10,7 @@
"cashier-stripe-development",
"laravel-best-practices",
"configuring-horizon",
+ "mcp-development",
"socialite-development",
"wayfinder-development",
"pest-testing",
diff --git a/bootstrap/app.php b/bootstrap/app.php
index 94282876..b0957dd7 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -4,6 +4,7 @@
use App\Http\Middleware\EnsureSubscribed;
use App\Http\Middleware\HandleAppearance;
use App\Http\Middleware\HandleInertiaRequests;
+use App\Http\Middleware\Mcp\AuthenticateMcpToken;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
@@ -35,6 +36,7 @@
$middleware->alias([
'subscribed' => EnsureSubscribed::class,
'api.auth' => AuthenticateApiToken::class,
+ 'mcp.auth' => AuthenticateMcpToken::class,
]);
$middleware->preventRequestForgery(except: [
diff --git a/bootstrap/providers.php b/bootstrap/providers.php
index 2345aa62..5afbb957 100644
--- a/bootstrap/providers.php
+++ b/bootstrap/providers.php
@@ -2,8 +2,10 @@
use App\Providers\AppServiceProvider;
use App\Providers\HorizonServiceProvider;
+use App\Providers\TelescopeServiceProvider;
return [
AppServiceProvider::class,
HorizonServiceProvider::class,
+ TelescopeServiceProvider::class,
];
diff --git a/composer.json b/composer.json
index a18b459e..1599fd75 100644
--- a/composer.json
+++ b/composer.json
@@ -35,10 +35,12 @@
"require": {
"php": "^8.2",
"inertiajs/inertia-laravel": "^3.0",
+ "laravel/ai": "^0.4.2",
"laravel/boost": "^2.0",
"laravel/cashier": "^16.2",
"laravel/framework": "^13.0",
"laravel/horizon": "^5.42",
+ "laravel/mcp": "^0.6.4",
"laravel/nightwatch": "^1.22",
"laravel/reverb": "^1.0",
"laravel/socialite": "^5.24",
@@ -59,6 +61,7 @@
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
"laravel/sail": "*",
+ "laravel/telescope": "^5.19",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^4.4",
diff --git a/composer.lock b/composer.lock
index 562d52c5..174bd059 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "222ac114e81a021efda803dd571fb2d2",
+ "content-hash": "5408bdeb64bc5066a115e08ba7f378f3",
"packages": [
{
"name": "aws/aws-crt-php",
@@ -1518,6 +1518,72 @@
},
"time": "2026-03-25T21:07:46+00:00"
},
+ {
+ "name": "laravel/ai",
+ "version": "v0.4.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/ai.git",
+ "reference": "91441b6ae5bc995f21bb3043744860bd3043e78c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/ai/zipball/91441b6ae5bc995f21bb3043744860bd3043e78c",
+ "reference": "91441b6ae5bc995f21bb3043744860bd3043e78c",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/console": "^12.0|^13.0",
+ "illuminate/container": "^12.0|^13.0",
+ "illuminate/contracts": "^12.0|^13.0",
+ "illuminate/filesystem": "^12.0|^13.0",
+ "illuminate/json-schema": "^12.0|^13.0",
+ "illuminate/support": "^12.0|^13.0",
+ "laravel/prompts": "^0.3.6",
+ "laravel/serializable-closure": "^2.0",
+ "php": "^8.3",
+ "prism-php/prism": "^0.99.0"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.26",
+ "mockery/mockery": "^1.6.12",
+ "orchestra/testbench": "^10.6|^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Ai\\AiServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "functions.php"
+ ],
+ "psr-4": {
+ "Laravel\\Ai\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "The official AI SDK for Laravel.",
+ "homepage": "https://github.com/laravel/ai",
+ "keywords": [
+ "ai",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/ai/issues",
+ "source": "https://github.com/laravel/ai"
+ },
+ "time": "2026-03-27T18:24:41+00:00"
+ },
{
"name": "laravel/boost",
"version": "v2.4.1",
@@ -4531,6 +4597,85 @@
],
"time": "2026-03-09T20:33:04+00:00"
},
+ {
+ "name": "prism-php/prism",
+ "version": "v0.99.22",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/prism-php/prism.git",
+ "reference": "989f67567aef69c613eae6e932d615fb96e2f5d7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/prism-php/prism/zipball/989f67567aef69c613eae6e932d615fb96e2f5d7",
+ "reference": "989f67567aef69c613eae6e932d615fb96e2f5d7",
+ "shasum": ""
+ },
+ "require": {
+ "ext-fileinfo": "*",
+ "laravel/framework": "^11.0|^12.0|^13.0",
+ "php": "^8.2"
+ },
+ "require-dev": {
+ "brianium/paratest": "^7.8.4",
+ "laravel/mcp": "^0.6.0",
+ "laravel/pint": "^1.14",
+ "mockery/mockery": "^1.6",
+ "orchestra/testbench": "^10",
+ "pestphp/pest": "^3.0",
+ "pestphp/pest-plugin-arch": "^3.0",
+ "pestphp/pest-plugin-laravel": "^3.0",
+ "phpstan/extension-installer": "^1.3",
+ "phpstan/phpdoc-parser": "^2.0",
+ "phpstan/phpstan": "2.1.34",
+ "phpstan/phpstan-deprecation-rules": "^2.0",
+ "projektgopher/whisky": "^0.7.0",
+ "rector/rector": "2.3.3",
+ "spatie/laravel-ray": "^1.39",
+ "symplify/rule-doc-generator-contracts": "^11.2"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "aliases": {
+ "PrismServer": "Prism\\Prism\\Facades\\PrismServer"
+ },
+ "providers": [
+ "Prism\\Prism\\PrismServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/helpers.php"
+ ],
+ "psr-4": {
+ "Prism\\Prism\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "TJ Miller",
+ "email": "hello@echolabs.dev"
+ }
+ ],
+ "description": "A powerful Laravel package for integrating Large Language Models (LLMs) into your applications.",
+ "support": {
+ "issues": "https://github.com/prism-php/prism/issues",
+ "source": "https://github.com/prism-php/prism/tree/v0.99.22"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sixlive",
+ "type": "github"
+ }
+ ],
+ "time": "2026-03-12T17:55:23+00:00"
+ },
{
"name": "psr/clock",
"version": "1.0.0",
@@ -9984,6 +10129,75 @@
},
"time": "2026-03-23T15:56:34+00:00"
},
+ {
+ "name": "laravel/telescope",
+ "version": "v5.19.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/telescope.git",
+ "reference": "5e95df170d14e03dd74c4b744969cf01f67a050b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/telescope/zipball/5e95df170d14e03dd74c4b744969cf01f67a050b",
+ "reference": "5e95df170d14e03dd74c4b744969cf01f67a050b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "laravel/framework": "^8.37|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "laravel/sentinel": "^1.0",
+ "php": "^8.0",
+ "symfony/console": "^5.3|^6.0|^7.0|^8.0",
+ "symfony/var-dumper": "^5.0|^6.0|^7.0|^8.0"
+ },
+ "require-dev": {
+ "ext-gd": "*",
+ "guzzlehttp/guzzle": "^6.0|^7.0",
+ "laravel/octane": "^1.4|^2.0",
+ "orchestra/testbench": "^6.47.1|^7.55|^8.36|^9.15|^10.8|^11.0",
+ "phpstan/phpstan": "^1.10"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Telescope\\TelescopeServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Telescope\\": "src/",
+ "Laravel\\Telescope\\Database\\Factories\\": "database/factories/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ },
+ {
+ "name": "Mohamed Said",
+ "email": "mohamed@laravel.com"
+ }
+ ],
+ "description": "An elegant debug assistant for the Laravel framework.",
+ "keywords": [
+ "debugging",
+ "laravel",
+ "monitoring"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/telescope/issues",
+ "source": "https://github.com/laravel/telescope/tree/v5.19.0"
+ },
+ "time": "2026-03-24T18:37:14+00:00"
+ },
{
"name": "mockery/mockery",
"version": "1.6.12",
diff --git a/config/ai.php b/config/ai.php
new file mode 100644
index 00000000..09ff5e07
--- /dev/null
+++ b/config/ai.php
@@ -0,0 +1,130 @@
+ 'openai',
+ 'default_for_images' => 'gemini',
+ 'default_for_audio' => 'openai',
+ 'default_for_transcription' => 'openai',
+ 'default_for_embeddings' => 'openai',
+ 'default_for_reranking' => 'cohere',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Caching
+ |--------------------------------------------------------------------------
+ |
+ | Below you may configure caching strategies for AI related operations
+ | such as embedding generation. You are free to adjust these values
+ | based on your application's available caching stores and needs.
+ |
+ */
+
+ 'caching' => [
+ 'embeddings' => [
+ 'cache' => false,
+ 'store' => env('CACHE_STORE', 'database'),
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | AI Providers
+ |--------------------------------------------------------------------------
+ |
+ | Below are each of your AI providers defined for this application. Each
+ | represents an AI provider and API key combination which can be used
+ | to perform tasks like text, image, and audio creation via agents.
+ |
+ */
+
+ 'providers' => [
+ 'anthropic' => [
+ 'driver' => 'anthropic',
+ 'key' => env('ANTHROPIC_API_KEY'),
+ ],
+
+ 'azure' => [
+ 'driver' => 'azure',
+ 'key' => env('AZURE_OPENAI_API_KEY'),
+ 'url' => env('AZURE_OPENAI_URL'),
+ 'api_version' => env('AZURE_OPENAI_API_VERSION', '2024-10-21'),
+ 'deployment' => env('AZURE_OPENAI_DEPLOYMENT', 'gpt-4o'),
+ 'embedding_deployment' => env('AZURE_OPENAI_EMBEDDING_DEPLOYMENT', 'text-embedding-3-small'),
+ ],
+
+ 'cohere' => [
+ 'driver' => 'cohere',
+ 'key' => env('COHERE_API_KEY'),
+ ],
+
+ 'deepseek' => [
+ 'driver' => 'deepseek',
+ 'key' => env('DEEPSEEK_API_KEY'),
+ ],
+
+ 'eleven' => [
+ 'driver' => 'eleven',
+ 'key' => env('ELEVENLABS_API_KEY'),
+ ],
+
+ 'gemini' => [
+ 'driver' => 'gemini',
+ 'key' => env('GEMINI_API_KEY'),
+ ],
+
+ 'groq' => [
+ 'driver' => 'groq',
+ 'key' => env('GROQ_API_KEY'),
+ ],
+
+ 'jina' => [
+ 'driver' => 'jina',
+ 'key' => env('JINA_API_KEY'),
+ ],
+
+ 'mistral' => [
+ 'driver' => 'mistral',
+ 'key' => env('MISTRAL_API_KEY'),
+ ],
+
+ 'ollama' => [
+ 'driver' => 'ollama',
+ 'key' => env('OLLAMA_API_KEY', ''),
+ 'url' => env('OLLAMA_BASE_URL', 'http://localhost:11434'),
+ ],
+
+ 'openai' => [
+ 'driver' => 'openai',
+ 'key' => env('OPENAI_API_KEY'),
+ 'url' => env('OPENAI_URL', 'https://api.openai.com/v1'),
+ ],
+
+ 'openrouter' => [
+ 'driver' => 'openrouter',
+ 'key' => env('OPENROUTER_API_KEY'),
+ ],
+
+ 'voyageai' => [
+ 'driver' => 'voyageai',
+ 'key' => env('VOYAGEAI_API_KEY'),
+ ],
+
+ 'xai' => [
+ 'driver' => 'xai',
+ 'key' => env('XAI_API_KEY'),
+ ],
+ ],
+
+];
diff --git a/config/telescope.php b/config/telescope.php
new file mode 100644
index 00000000..6250e787
--- /dev/null
+++ b/config/telescope.php
@@ -0,0 +1,212 @@
+ env('TELESCOPE_ENABLED', true),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Telescope Domain
+ |--------------------------------------------------------------------------
+ |
+ | This is the subdomain where Telescope will be accessible from. If the
+ | setting is null, Telescope will reside under the same domain as the
+ | application. Otherwise, this value will be used as the subdomain.
+ |
+ */
+
+ 'domain' => env('TELESCOPE_DOMAIN'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Telescope Path
+ |--------------------------------------------------------------------------
+ |
+ | This is the URI path where Telescope will be accessible from. Feel free
+ | to change this path to anything you like. Note that the URI will not
+ | affect the paths of its internal API that aren't exposed to users.
+ |
+ */
+
+ 'path' => env('TELESCOPE_PATH', 'telescope'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Telescope Storage Driver
+ |--------------------------------------------------------------------------
+ |
+ | This configuration options determines the storage driver that will
+ | be used to store Telescope's data. In addition, you may set any
+ | custom options as needed by the particular driver you choose.
+ |
+ */
+
+ 'driver' => env('TELESCOPE_DRIVER', 'database'),
+
+ 'storage' => [
+ 'database' => [
+ 'connection' => env('DB_CONNECTION', 'mysql'),
+ 'chunk' => 1000,
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Telescope Queue
+ |--------------------------------------------------------------------------
+ |
+ | This configuration options determines the queue connection and queue
+ | which will be used to process ProcessPendingUpdate jobs. This can
+ | be changed if you would prefer to use a non-default connection.
+ |
+ */
+
+ 'queue' => [
+ 'connection' => env('TELESCOPE_QUEUE_CONNECTION'),
+ 'queue' => env('TELESCOPE_QUEUE'),
+ 'delay' => env('TELESCOPE_QUEUE_DELAY', 10),
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Telescope Route Middleware
+ |--------------------------------------------------------------------------
+ |
+ | These middleware will be assigned to every Telescope route, giving you
+ | the chance to add your own middleware to this list or change any of
+ | the existing middleware. Or, you can simply stick with this list.
+ |
+ */
+
+ 'middleware' => [
+ 'web',
+ Authorize::class,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Allowed / Ignored Paths & Commands
+ |--------------------------------------------------------------------------
+ |
+ | The following array lists the URI paths and Artisan commands that will
+ | not be watched by Telescope. In addition to this list, some Laravel
+ | commands, like migrations and queue commands, are always ignored.
+ |
+ */
+
+ 'only_paths' => [
+ // 'api/*'
+ ],
+
+ 'ignore_paths' => [
+ 'livewire*',
+ 'nova-api*',
+ 'pulse*',
+ '_boost*',
+ '.well-known*',
+ ],
+
+ 'ignore_commands' => [
+ //
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Telescope Watchers
+ |--------------------------------------------------------------------------
+ |
+ | The following array lists the "watchers" that will be registered with
+ | Telescope. The watchers gather the application's profile data when
+ | a request or task is executed. Feel free to customize this list.
+ |
+ */
+
+ 'watchers' => [
+ Watchers\BatchWatcher::class => env('TELESCOPE_BATCH_WATCHER', true),
+
+ Watchers\CacheWatcher::class => [
+ 'enabled' => env('TELESCOPE_CACHE_WATCHER', true),
+ 'hidden' => [],
+ 'ignore' => [],
+ ],
+
+ Watchers\ClientRequestWatcher::class => [
+ 'enabled' => env('TELESCOPE_CLIENT_REQUEST_WATCHER', true),
+ 'ignore_hosts' => [],
+ ],
+
+ Watchers\CommandWatcher::class => [
+ 'enabled' => env('TELESCOPE_COMMAND_WATCHER', true),
+ 'ignore' => [],
+ ],
+
+ Watchers\DumpWatcher::class => [
+ 'enabled' => env('TELESCOPE_DUMP_WATCHER', true),
+ 'always' => env('TELESCOPE_DUMP_WATCHER_ALWAYS', false),
+ ],
+
+ Watchers\EventWatcher::class => [
+ 'enabled' => env('TELESCOPE_EVENT_WATCHER', true),
+ 'ignore' => [],
+ ],
+
+ Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true),
+
+ Watchers\GateWatcher::class => [
+ 'enabled' => env('TELESCOPE_GATE_WATCHER', true),
+ 'ignore_abilities' => [],
+ 'ignore_packages' => true,
+ 'ignore_paths' => [],
+ ],
+
+ Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true),
+
+ Watchers\LogWatcher::class => [
+ 'enabled' => env('TELESCOPE_LOG_WATCHER', true),
+ 'level' => 'error',
+ ],
+
+ Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true),
+
+ Watchers\ModelWatcher::class => [
+ 'enabled' => env('TELESCOPE_MODEL_WATCHER', true),
+ 'events' => ['eloquent.*'],
+ 'hydrations' => true,
+ ],
+
+ Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true),
+
+ Watchers\QueryWatcher::class => [
+ 'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
+ 'ignore_packages' => true,
+ 'ignore_paths' => [],
+ 'slow' => 100,
+ ],
+
+ Watchers\RedisWatcher::class => env('TELESCOPE_REDIS_WATCHER', true),
+
+ Watchers\RequestWatcher::class => [
+ 'enabled' => env('TELESCOPE_REQUEST_WATCHER', true),
+ 'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64),
+ 'ignore_http_methods' => [],
+ 'ignore_status_codes' => [],
+ ],
+
+ Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true),
+ Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true),
+ ],
+];
diff --git a/database/migrations/2026_03_29_232520_create_agent_conversations_table.php b/database/migrations/2026_03_29_232520_create_agent_conversations_table.php
new file mode 100644
index 00000000..9085f86d
--- /dev/null
+++ b/database/migrations/2026_03_29_232520_create_agent_conversations_table.php
@@ -0,0 +1,50 @@
+uuid('id')->primary();
+ $table->foreignId('user_id')->nullable();
+ $table->string('title');
+ $table->timestamps();
+
+ $table->index(['user_id', 'updated_at']);
+ });
+
+ Schema::create('agent_conversation_messages', function (Blueprint $table) {
+ $table->uuid('id')->primary();
+ $table->string('conversation_id', 36)->index();
+ $table->foreignId('user_id')->nullable();
+ $table->string('agent');
+ $table->string('role', 25);
+ $table->text('content');
+ $table->text('attachments');
+ $table->text('tool_calls');
+ $table->text('tool_results');
+ $table->text('usage');
+ $table->text('meta');
+ $table->timestamps();
+
+ $table->index(['conversation_id', 'user_id', 'updated_at'], 'conversation_index');
+ $table->index(['user_id']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('agent_conversations');
+ Schema::dropIfExists('agent_conversation_messages');
+ }
+};
diff --git a/database/migrations/2026_03_29_233024_create_telescope_entries_table.php b/database/migrations/2026_03_29_233024_create_telescope_entries_table.php
new file mode 100644
index 00000000..031b6f47
--- /dev/null
+++ b/database/migrations/2026_03_29_233024_create_telescope_entries_table.php
@@ -0,0 +1,70 @@
+getConnection());
+
+ $schema->create('telescope_entries', function (Blueprint $table) {
+ $table->bigIncrements('sequence');
+ $table->uuid('uuid');
+ $table->uuid('batch_id');
+ $table->string('family_hash')->nullable();
+ $table->boolean('should_display_on_index')->default(true);
+ $table->string('type', 20);
+ $table->longText('content');
+ $table->dateTime('created_at')->nullable();
+
+ $table->unique('uuid');
+ $table->index('batch_id');
+ $table->index('family_hash');
+ $table->index('created_at');
+ $table->index(['type', 'should_display_on_index']);
+ });
+
+ $schema->create('telescope_entries_tags', function (Blueprint $table) {
+ $table->uuid('entry_uuid');
+ $table->string('tag');
+
+ $table->primary(['entry_uuid', 'tag']);
+ $table->index('tag');
+
+ $table->foreign('entry_uuid')
+ ->references('uuid')
+ ->on('telescope_entries')
+ ->cascadeOnDelete();
+ });
+
+ $schema->create('telescope_monitoring', function (Blueprint $table) {
+ $table->string('tag')->primary();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ $schema = Schema::connection($this->getConnection());
+
+ $schema->dropIfExists('telescope_entries_tags');
+ $schema->dropIfExists('telescope_entries');
+ $schema->dropIfExists('telescope_monitoring');
+ }
+};
diff --git a/routes/mcp.php b/routes/ai.php
similarity index 56%
rename from routes/mcp.php
rename to routes/ai.php
index 92f6d3b7..79c70e99 100644
--- a/routes/mcp.php
+++ b/routes/ai.php
@@ -2,13 +2,16 @@
declare(strict_types=1);
+use App\Mcp\Servers\TryPostServer;
use Illuminate\Support\Facades\Route;
+use Laravel\Mcp\Facades\Mcp;
Route::group(
[
'domain' => 'mcp.'.parse_url(config('app.url'), PHP_URL_HOST),
],
function () {
- //
+ Mcp::web('/trypost', TryPostServer::class)
+ ->middleware('mcp.auth');
}
);
diff --git a/stubs/agent-middleware.stub b/stubs/agent-middleware.stub
new file mode 100644
index 00000000..c1a50f4f
--- /dev/null
+++ b/stubs/agent-middleware.stub
@@ -0,0 +1,20 @@
+then(function (AgentResponse $response) {
+ // ...
+ });
+ }
+}
diff --git a/stubs/agent.stub b/stubs/agent.stub
new file mode 100644
index 00000000..06471d5d
--- /dev/null
+++ b/stubs/agent.stub
@@ -0,0 +1,44 @@
+ $schema->string()->required(),
+ ];
+ }
+}
diff --git a/stubs/tool.stub b/stubs/tool.stub
new file mode 100644
index 00000000..e0960219
--- /dev/null
+++ b/stubs/tool.stub
@@ -0,0 +1,37 @@
+ $schema->string()->required(),
+ ];
+ }
+}
diff --git a/tests/Feature/Api/PostApiTest.php b/tests/Feature/Api/PostApiTest.php
new file mode 100644
index 00000000..26b6d55f
--- /dev/null
+++ b/tests/Feature/Api/PostApiTest.php
@@ -0,0 +1,104 @@
+user = User::factory()->create();
+ $this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
+ $this->workspace->members()->attach($this->user->id, ['role' => 'owner']);
+
+ $plainToken = 'tp_'.Str::random(48);
+ $this->plainToken = $plainToken;
+ $this->apiToken = ApiToken::factory()->create([
+ 'workspace_id' => $this->workspace->id,
+ 'token_lookup' => substr($plainToken, 3, 16),
+ 'token_hash' => Hash::make($plainToken),
+ ]);
+
+ $this->socialAccount = SocialAccount::factory()->create([
+ 'workspace_id' => $this->workspace->id,
+ 'platform' => Platform::LinkedIn,
+ ]);
+});
+
+it('lists posts', function () {
+ $post = Post::factory()->create([
+ 'workspace_id' => $this->workspace->id,
+ 'user_id' => $this->user->id,
+ ]);
+
+ $this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
+ ->getJson(route('api.posts.index'))
+ ->assertOk()
+ ->assertJsonCount(1, 'data');
+});
+
+it('shows a post', function () {
+ $post = Post::factory()->create([
+ 'workspace_id' => $this->workspace->id,
+ 'user_id' => $this->user->id,
+ ]);
+
+ $this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
+ ->getJson(route('api.posts.show', $post))
+ ->assertOk()
+ ->assertJsonPath('id', $post->id);
+});
+
+it('cannot show post from another workspace', function () {
+ $otherWorkspace = Workspace::factory()->create();
+ $post = Post::factory()->create([
+ 'workspace_id' => $otherWorkspace->id,
+ 'user_id' => $this->user->id,
+ ]);
+
+ $this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
+ ->getJson(route('api.posts.show', $post))
+ ->assertNotFound();
+});
+
+it('creates a post', function () {
+ $this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
+ ->postJson(route('api.posts.store'), [
+ 'date' => now()->addDay()->format('Y-m-d'),
+ ])
+ ->assertCreated()
+ ->assertJsonPath('status', PostStatus::Draft->value);
+
+ expect(Post::where('workspace_id', $this->workspace->id)->count())->toBe(1);
+});
+
+it('deletes a post', function () {
+ $post = Post::factory()->create([
+ 'workspace_id' => $this->workspace->id,
+ 'user_id' => $this->user->id,
+ ]);
+
+ $this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
+ ->deleteJson(route('api.posts.destroy', $post))
+ ->assertNoContent();
+
+ expect(Post::find($post->id))->toBeNull();
+});
+
+it('cannot delete post from another workspace', function () {
+ $otherWorkspace = Workspace::factory()->create();
+ $post = Post::factory()->create([
+ 'workspace_id' => $otherWorkspace->id,
+ 'user_id' => $this->user->id,
+ ]);
+
+ $this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
+ ->deleteJson(route('api.posts.destroy', $post))
+ ->assertNotFound();
+});