feat: implement MCP server with tools, Post API tests, auth middleware
- Create TryPostServer MCP server with 17 tools: Post (List, Get, Create, Delete), Hashtag (List, Create, Update, Delete), Label (List, Create, Update, Delete), Workspace (Get), ApiKey (List, Create, Delete) - Create AuthenticateMcpToken middleware (logs in workspace owner) - Register mcp.auth middleware alias in bootstrap/app.php - Create routes/ai.php with mcp.trypost.test subdomain - Add PostApiTest with 6 tests (list, show, create, delete, isolation) - Fix PostApiTest assertions for pagination/resource wrapping - 704 tests passing, frontend build passing
This commit is contained in:
parent
039ed02b42
commit
fabce14aad
42 changed files with 1962 additions and 201 deletions
|
|
@ -33,9 +33,9 @@ ## Basic Usage
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php artisan vendor:publish --tag="cashier-migrations"
|
vendor/bin/sail artisan vendor:publish --tag="cashier-migrations"
|
||||||
php artisan migrate
|
vendor/bin/sail artisan migrate
|
||||||
php artisan vendor:publish --tag="cashier-config"
|
vendor/bin/sail artisan vendor:publish --tag="cashier-config"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ ## Basic Usage
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php artisan horizon:install
|
vendor/bin/sail artisan horizon:install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Supervisor Configuration
|
### Supervisor Configuration
|
||||||
|
|
@ -70,7 +70,7 @@ ### Dashboard Authorization
|
||||||
|
|
||||||
## Verification
|
## 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
|
2. Confirm dashboard access is restricted as expected
|
||||||
3. Check that metrics populate after scheduling `horizon:snapshot`
|
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.
|
- 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 `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 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.
|
- Always use `search-docs` for the latest Horizon documentation rather than relying on this skill alone.
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
name: inertia-vue-development
|
name: inertia-vue-development
|
||||||
description: "Develops Inertia.js v2 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation."
|
description: "Develops Inertia.js v3 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using <Link>, <Form>, 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."
|
||||||
license: MIT
|
license: MIT
|
||||||
metadata:
|
metadata:
|
||||||
author: laravel
|
author: laravel
|
||||||
|
|
@ -8,9 +8,19 @@
|
||||||
|
|
||||||
# Inertia Vue Development
|
# Inertia Vue Development
|
||||||
|
|
||||||
|
## When to Apply
|
||||||
|
|
||||||
|
Activate this skill when:
|
||||||
|
|
||||||
|
- Creating or modifying Vue page components for Inertia
|
||||||
|
- Working with forms in Vue (using `<Form>`, `useForm`, or `useHttp`)
|
||||||
|
- Implementing client-side navigation with `<Link>` or `router`
|
||||||
|
- Using v3 features: deferred props, prefetching, optimistic updates, instant visits, layout props, HTTP requests, WhenVisible, InfiniteScroll, once props, flash data, or polling
|
||||||
|
- Building Vue-specific features with the Inertia protocol
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Use `search-docs` for detailed Inertia v2 Vue patterns and documentation.
|
Use `search-docs` for detailed Inertia v3 Vue patterns and documentation.
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
|
|
@ -20,8 +30,6 @@ ### Page Components Location
|
||||||
|
|
||||||
### Page Component Structure
|
### Page Component Structure
|
||||||
|
|
||||||
Important: Vue components must have a single root element.
|
|
||||||
|
|
||||||
<!-- Basic Vue Page Component -->
|
<!-- Basic Vue Page Component -->
|
||||||
```vue
|
```vue
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
@ -271,7 +279,137 @@ ### `useForm` Composable
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inertia v2 Features
|
## Inertia v3 Features
|
||||||
|
|
||||||
|
### HTTP Requests
|
||||||
|
|
||||||
|
Use the `useHttp` hook for standalone HTTP requests that do not trigger Inertia page visits. It provides the same developer experience as `useForm`, but for plain JSON endpoints.
|
||||||
|
|
||||||
|
<!-- useHttp Example -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { useHttp } from '@inertiajs/vue3'
|
||||||
|
|
||||||
|
const http = useHttp({
|
||||||
|
query: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
function search() {
|
||||||
|
http.get('/api/search', {
|
||||||
|
onSuccess: (response) => {
|
||||||
|
console.log(response)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input v-model="http.query" @input="search" />
|
||||||
|
<div v-if="http.processing">Searching...</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optimistic Updates
|
||||||
|
|
||||||
|
Apply data changes instantly before the server responds, with automatic rollback on failure:
|
||||||
|
|
||||||
|
<!-- Optimistic Update with Router -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { router } from '@inertiajs/vue3'
|
||||||
|
|
||||||
|
function like(post) {
|
||||||
|
router.optimistic((props) => ({
|
||||||
|
post: {
|
||||||
|
...props.post,
|
||||||
|
likes: props.post.likes + 1,
|
||||||
|
},
|
||||||
|
})).post(`/posts/${post.id}/like`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Optimistic updates also work with `useForm` and the `<Form>` component:
|
||||||
|
|
||||||
|
<!-- Optimistic Update with Form Component -->
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
action="/todos"
|
||||||
|
method="post"
|
||||||
|
:optimistic="(props, data) => ({
|
||||||
|
todos: [...props.todos, { id: Date.now(), name: data.name, done: false }],
|
||||||
|
})"
|
||||||
|
>
|
||||||
|
<input type="text" name="name" />
|
||||||
|
<button type="submit">Add Todo</button>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instant Visits
|
||||||
|
|
||||||
|
Navigate to a new page immediately without waiting for the server response. The target component renders right away with shared props, while page-specific props load in the background.
|
||||||
|
|
||||||
|
<!-- Instant Visit with Link -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { Link } from '@inertiajs/vue3'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Link href="/dashboard" component="Dashboard">Dashboard</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/posts/1"
|
||||||
|
component="Posts/Show"
|
||||||
|
:page-props="{ post: { id: 1, title: 'My Post' } }"
|
||||||
|
>
|
||||||
|
View Post
|
||||||
|
</Link>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layout Props
|
||||||
|
|
||||||
|
Share dynamic data between pages and persistent layouts:
|
||||||
|
|
||||||
|
<!-- Layout Props in Layout -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
withDefaults(defineProps({
|
||||||
|
title: String,
|
||||||
|
showSidebar: Boolean,
|
||||||
|
}), {
|
||||||
|
title: 'My App',
|
||||||
|
showSidebar: true,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header>{{ title }}</header>
|
||||||
|
<aside v-if="showSidebar">Sidebar</aside>
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Setting Layout Props from Page -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { setLayoutProps } from '@inertiajs/vue3'
|
||||||
|
|
||||||
|
setLayoutProps({
|
||||||
|
title: 'Dashboard',
|
||||||
|
showSidebar: false,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
### Deferred Props
|
### Deferred Props
|
||||||
|
|
||||||
|
|
@ -358,42 +496,69 @@ ### Polling
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
- `autoStart` (default `true`) — set to `false` to start polling manually via the returned `start()` function
|
- `autoStart` (default `true`) - set to `false` to start polling manually via the returned `start()` function
|
||||||
- `keepAlive` (default `false`) — set to `true` to prevent throttling when the browser tab is inactive
|
- `keepAlive` (default `false`) - set to `true` to prevent throttling when the browser tab is inactive
|
||||||
|
|
||||||
### WhenVisible (Infinite Scroll)
|
### WhenVisible
|
||||||
|
|
||||||
Load more data when user scrolls to a specific element:
|
Lazy-load a prop when an element scrolls into view. Useful for deferring expensive data that sits below the fold:
|
||||||
|
|
||||||
<!-- Infinite Scroll with WhenVisible -->
|
<!-- WhenVisible Example -->
|
||||||
```vue
|
```vue
|
||||||
<script setup>
|
<script setup>
|
||||||
import { WhenVisible } from '@inertiajs/vue3'
|
import { WhenVisible } from '@inertiajs/vue3'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
stats: Object
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
|
||||||
|
<WhenVisible data="stats" :buffer="200">
|
||||||
|
<template #fallback>
|
||||||
|
<div class="animate-pulse">Loading stats...</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #default="{ fetching }">
|
||||||
|
<div>
|
||||||
|
<p>Total Users: {{ stats.total_users }}</p>
|
||||||
|
<p>Revenue: {{ stats.revenue }}</p>
|
||||||
|
<span v-if="fetching">Refreshing...</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</WhenVisible>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### InfiniteScroll
|
||||||
|
|
||||||
|
Automatically load additional pages of paginated data as users scroll:
|
||||||
|
|
||||||
|
<!-- InfiniteScroll Example -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { InfiniteScroll } from '@inertiajs/vue3'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
users: Object
|
users: Object
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<InfiniteScroll data="users">
|
||||||
<div v-for="user in users.data" :key="user.id">
|
<div v-for="user in users.data" :key="user.id">
|
||||||
{{ user.name }}
|
{{ user.name }}
|
||||||
</div>
|
</div>
|
||||||
|
</InfiniteScroll>
|
||||||
<WhenVisible
|
|
||||||
v-if="users.next_page_url"
|
|
||||||
data="users"
|
|
||||||
:params="{ page: users.current_page + 1 }"
|
|
||||||
>
|
|
||||||
<template #fallback>
|
|
||||||
<div>Loading more...</div>
|
|
||||||
</template>
|
|
||||||
</WhenVisible>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The server must use `Inertia::scroll()` to configure the paginated data. Use the `search-docs` tool with a query of `infinite scroll` for detailed guidance on buffers, manual loading, reverse mode, and custom trigger elements.
|
||||||
|
|
||||||
## Server-Side Patterns
|
## Server-Side Patterns
|
||||||
|
|
||||||
Server-side patterns (Inertia::render, props, middleware) are covered in inertia-laravel guidelines.
|
Server-side patterns (Inertia::render, props, middleware) are covered in inertia-laravel guidelines.
|
||||||
|
|
@ -405,4 +570,6 @@ ## Common Pitfalls
|
||||||
- Forgetting to add loading states (skeleton screens) when using deferred props
|
- Forgetting to add loading states (skeleton screens) when using deferred props
|
||||||
- Not handling the `undefined` state of deferred props before data loads
|
- Not handling the `undefined` state of deferred props before data loads
|
||||||
- Using `<form>` without preventing default submission (use `<Form>` component or `@submit.prevent`)
|
- Using `<form>` without preventing default submission (use `<Form>` component or `@submit.prevent`)
|
||||||
- Forgetting to check if `<Form>` component is available in your Inertia version
|
- Forgetting to check if `<Form>` 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
|
||||||
|
|
@ -8,148 +8,88 @@
|
||||||
|
|
||||||
# MCP Development
|
# 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.
|
||||||
|
|
||||||
<!-- Search MCP Documentation -->
|
## Basic Usage
|
||||||
```bash
|
|
||||||
|
|
||||||
# Example searches
|
Register MCP servers in `routes/ai.php`:
|
||||||
|
|
||||||
search-docs(['mcp tools', 'mcp resources', 'mcp validation'])
|
<!-- Register MCP Server -->
|
||||||
```
|
```php
|
||||||
|
use Laravel\Mcp\Facades\Mcp;
|
||||||
## Quick Reference
|
|
||||||
|
Mcp::web();
|
||||||
### Artisan Commands
|
```
|
||||||
|
|
||||||
Create MCP Primitives"
|
### Creating MCP Primitives
|
||||||
```bash
|
|
||||||
php artisan make:mcp-tool ToolName
|
Create MCP tools, resources, prompts, and servers using artisan commands:
|
||||||
php artisan make:mcp-resource ResourceName
|
|
||||||
php artisan make:mcp-prompt PromptName
|
```bash
|
||||||
php artisan make:mcp-server ServerName
|
vendor/bin/sail artisan make:mcp-tool ToolName # Create a tool
|
||||||
```
|
|
||||||
|
vendor/bin/sail artisan make:mcp-resource ResourceName # Create a resource
|
||||||
### Basic Tool Implementation
|
|
||||||
|
vendor/bin/sail artisan make:mcp-prompt PromptName # Create a prompt
|
||||||
<!-- Tool Example -->
|
|
||||||
|
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
|
||||||
|
|
||||||
|
<!-- MCP Tool Example -->
|
||||||
```php
|
```php
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
|
||||||
use Laravel\Mcp\Request;
|
|
||||||
use Laravel\Mcp\Response;
|
|
||||||
use Laravel\Mcp\Server\Tool;
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
use Laravel\Mcp\Server\Request;
|
||||||
|
use Laravel\Mcp\Server\Response;
|
||||||
|
|
||||||
class MyTool extends Tool
|
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
|
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
|
||||||
|
|
||||||
<!-- Resource Example -->
|
Each MCP server must explicitly declare the tools, resources, and prompts it exposes.
|
||||||
|
|
||||||
|
<!-- Register Primitives in MCP Server -->
|
||||||
```php
|
```php
|
||||||
use Laravel\Mcp\Response;
|
use Laravel\Mcp\Server;
|
||||||
use Laravel\Mcp\Server\Resource;
|
|
||||||
|
|
||||||
class MyResource extends Resource
|
class AppServer extends Server
|
||||||
{
|
{
|
||||||
protected string $description = 'Resource description';
|
protected array $tools = [
|
||||||
protected string $uri = 'file://path/to/resource';
|
\App\Mcp\Tools\MyTool::class,
|
||||||
protected string $mimeType = 'text/markdown';
|
];
|
||||||
|
|
||||||
public function handle(): Response
|
protected array $resources = [
|
||||||
{
|
\App\Mcp\Resources\MyResource::class,
|
||||||
return Response::text($content);
|
];
|
||||||
}
|
|
||||||
|
protected array $prompts = [
|
||||||
|
\App\Mcp\Prompts\MyPrompt::class,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Response Methods
|
## Verification
|
||||||
|
|
||||||
<!-- Available Responses -->
|
1. Check `routes/ai.php` for proper registration
|
||||||
```php
|
2. Test tool via MCP client
|
||||||
Response::text('Text content');
|
|
||||||
Response::error('Error message');
|
|
||||||
Response::structured(['key' => 'value']);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing MCP Primitives
|
|
||||||
|
|
||||||
Test tools, resources, and prompts directly on their server:
|
|
||||||
|
|
||||||
<!-- Test MCP Primitives -->
|
|
||||||
```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:
|
|
||||||
|
|
||||||
<!--Launch MCP 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
|
|
||||||
|
|
||||||
<!-- Correct 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;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Pitfalls
|
## Common Pitfalls
|
||||||
|
|
||||||
- **Not using `search-docs` before implementation**
|
- Running `mcp:start` command (it hangs waiting for input)
|
||||||
- Wrong imports: `Laravel\Mcp\Server\Request` (wrong) vs `Laravel\Mcp\Request` (correct)
|
- Using HTTPS locally with Node-based MCP clients
|
||||||
- Forgetting `schema()` method for tools with parameters
|
- Not using `search-docs` for the latest MCP documentation
|
||||||
- Missing required properties: `$description`, `$uri`, `$mimeType`
|
- Not registering MCP server routes in `routes/ai.php`
|
||||||
- Wrong response pattern: `new Response()` instead of `Response::text()`
|
- Do not register `ai.php` in `bootstrap.php`; it is registered automatically.
|
||||||
- Running `mcp:start` command locally (hangs waiting for stdin)
|
|
||||||
|
|
@ -16,7 +16,7 @@ ## Basic Usage
|
||||||
|
|
||||||
### Creating Tests
|
### 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
|
### Test Organization
|
||||||
|
|
||||||
|
|
@ -35,9 +35,9 @@ ### Basic Test Structure
|
||||||
|
|
||||||
### Running Tests
|
### Running Tests
|
||||||
|
|
||||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
- Run minimal tests with filter before finalizing: `vendor/bin/sail artisan test --compact --filter=testName`.
|
||||||
- Run all tests: `php artisan test --compact`.
|
- Run all tests: `vendor/bin/sail artisan test --compact`.
|
||||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
- Run file: `vendor/bin/sail artisan test --compact tests/Feature/ExampleTest.php`.
|
||||||
|
|
||||||
## Assertions
|
## Assertions
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ ### Generate Routes
|
||||||
|
|
||||||
Run after route changes if Vite plugin isn't installed:
|
Run after route changes if Vite plugin isn't installed:
|
||||||
```bash
|
```bash
|
||||||
php artisan wayfinder:generate --no-interaction
|
vendor/bin/sail artisan wayfinder:generate --no-interaction
|
||||||
```
|
```
|
||||||
For form helpers, use `--with-form` flag:
|
For form helpers, use `--with-form` flag:
|
||||||
```bash
|
```bash
|
||||||
php artisan wayfinder:generate --with-form --no-interaction
|
vendor/bin/sail artisan wayfinder:generate --with-form --no-interaction
|
||||||
```
|
```
|
||||||
|
|
||||||
### Import Patterns
|
### Import Patterns
|
||||||
|
|
@ -69,7 +69,7 @@ ## Wayfinder + Inertia
|
||||||
|
|
||||||
## Verification
|
## 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
|
2. Check TypeScript imports resolve correctly
|
||||||
3. Verify route URLs match expected paths
|
3. Verify route URLs match expected paths
|
||||||
|
|
||||||
|
|
|
||||||
75
CLAUDE.md
75
CLAUDE.md
|
|
@ -12,24 +12,23 @@ ## Foundational Context
|
||||||
- php - 8.4
|
- php - 8.4
|
||||||
- inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3
|
- inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3
|
||||||
- laravel/ai (AI) - v0
|
- laravel/ai (AI) - v0
|
||||||
|
- laravel/boost (BOOST) - v2
|
||||||
- laravel/cashier (CASHIER) - v16
|
- laravel/cashier (CASHIER) - v16
|
||||||
- laravel/framework (LARAVEL) - v13
|
- laravel/framework (LARAVEL) - v13
|
||||||
- laravel/horizon (HORIZON) - v5
|
- laravel/horizon (HORIZON) - v5
|
||||||
- laravel/mcp (MCP) - v0
|
- laravel/mcp (MCP) - v0
|
||||||
- laravel/nightwatch (NIGHTWATCH) - v1
|
- laravel/nightwatch (NIGHTWATCH) - v1
|
||||||
- laravel/pennant (PENNANT) - v1
|
|
||||||
- laravel/prompts (PROMPTS) - v0
|
- laravel/prompts (PROMPTS) - v0
|
||||||
- laravel/reverb (REVERB) - v1
|
- laravel/reverb (REVERB) - v1
|
||||||
- laravel/socialite (SOCIALITE) - v5
|
- laravel/socialite (SOCIALITE) - v5
|
||||||
- laravel/wayfinder (WAYFINDER) - v0
|
- laravel/wayfinder (WAYFINDER) - v0
|
||||||
- laravel/boost (BOOST) - v2
|
|
||||||
- laravel/pail (PAIL) - v1
|
- laravel/pail (PAIL) - v1
|
||||||
- laravel/pint (PINT) - v1
|
- laravel/pint (PINT) - v1
|
||||||
- laravel/sail (SAIL) - v1
|
- laravel/sail (SAIL) - v1
|
||||||
- laravel/telescope (TELESCOPE) - v5
|
- laravel/telescope (TELESCOPE) - v5
|
||||||
- pestphp/pest (PEST) - v4
|
- pestphp/pest (PEST) - v4
|
||||||
- phpunit/phpunit (PHPUNIT) - v12
|
- phpunit/phpunit (PHPUNIT) - v12
|
||||||
- @inertiajs/vue3 (INERTIA_VUE) - v2
|
- @inertiajs/vue3 (INERTIA_VUE) - v3
|
||||||
- tailwindcss (TAILWINDCSS) - v4
|
- tailwindcss (TAILWINDCSS) - v4
|
||||||
- vue (VUE) - v3
|
- vue (VUE) - v3
|
||||||
- @laravel/echo-vue (ECHO_VUE) - v2
|
- @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.
|
- `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.
|
- `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.
|
- `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.
|
- `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.
|
|
||||||
- `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.
|
- `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
|
- `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.
|
- `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 <Link>, <Form>, 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 <Link>, <Form>, 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.
|
- `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
|
## Conventions
|
||||||
|
|
||||||
|
|
@ -72,7 +68,7 @@ ## Application Structure & Architecture
|
||||||
|
|
||||||
## Frontend Bundling
|
## 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
|
## Documentation Files
|
||||||
|
|
||||||
|
|
@ -110,22 +106,21 @@ ### Search Syntax
|
||||||
|
|
||||||
## Artisan
|
## 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.
|
- 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 `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
|
- 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: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
|
- 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.
|
- To check environment variables, read the `.env` file directly.
|
||||||
|
|
||||||
## Tinker
|
## 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.
|
- 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();'`
|
- Always use single quotes to prevent shell expansion: `vendor/bin/sail artisan tinker --execute 'Your::code();'`
|
||||||
- Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'`
|
- Double quotes for PHP strings inside: `vendor/bin/sail artisan tinker --execute 'User::where("active", true)->count();'`
|
||||||
|
|
||||||
=== php rules ===
|
=== php rules ===
|
||||||
|
|
||||||
# PHP
|
# 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.
|
- 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 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`
|
- 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.
|
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
|
||||||
- Use array shape type definitions in PHPDoc blocks.
|
- 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.
|
- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
|
||||||
- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start <service>`, `herd php:list`). Run `herd list` to discover all available commands.
|
- 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 ===
|
=== tests rules ===
|
||||||
|
|
||||||
# Test Enforcement
|
# 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.
|
- 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 ===
|
=== inertia-laravel/core rules ===
|
||||||
|
|
||||||
|
|
@ -174,13 +176,13 @@ # Inertia v3
|
||||||
|
|
||||||
# Do Things the Laravel Way
|
# 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`.
|
- 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 `php artisan make:class`.
|
- 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.
|
- 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
|
### 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
|
## 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.
|
- 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()`.
|
- 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
|
## 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 ===
|
=== wayfinder/core rules ===
|
||||||
|
|
||||||
|
|
@ -210,15 +212,15 @@ # Laravel Wayfinder
|
||||||
|
|
||||||
# Laravel Pint Code Formatter
|
# 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.
|
- 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/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
|
- 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/core rules ===
|
||||||
|
|
||||||
## Pest
|
## Pest
|
||||||
|
|
||||||
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
|
- This project uses Pest for testing. Create tests: `vendor/bin/sail artisan make:test --pest {name}`.
|
||||||
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
|
- Run tests: `vendor/bin/sail artisan test --compact` or filter: `vendor/bin/sail artisan test --compact --filter=testName`.
|
||||||
- Do NOT delete tests without approval.
|
- Do NOT delete tests without approval.
|
||||||
|
|
||||||
=== inertia-vue/core rules ===
|
=== inertia-vue/core rules ===
|
||||||
|
|
@ -226,23 +228,8 @@ ## Pest
|
||||||
# Inertia + Vue
|
# Inertia + Vue
|
||||||
|
|
||||||
Vue components must have a single root element.
|
Vue components must have a single root element.
|
||||||
|
|
||||||
- IMPORTANT: Activate `inertia-vue-development` when working with Inertia Vue client-side patterns.
|
- 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.
|
|
||||||
|
|
||||||
</laravel-boost-guidelines>
|
</laravel-boost-guidelines>
|
||||||
|
|
||||||
# Project-Specific Rules
|
# Project-Specific Rules
|
||||||
|
|
|
||||||
53
app/Http/Middleware/Mcp/AuthenticateMcpToken.php
Normal file
53
app/Http/Middleware/Mcp/AuthenticateMcpToken.php
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware\Mcp;
|
||||||
|
|
||||||
|
use App\Models\ApiToken;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class AuthenticateMcpToken
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
$token = $request->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
app/Mcp/Servers/TryPostServer.php
Normal file
66
app/Mcp/Servers/TryPostServer.php
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Servers;
|
||||||
|
|
||||||
|
use App\Mcp\Tools\ApiKey\CreateApiKeyTool;
|
||||||
|
use App\Mcp\Tools\ApiKey\DeleteApiKeyTool;
|
||||||
|
use App\Mcp\Tools\ApiKey\ListApiKeysTool;
|
||||||
|
use App\Mcp\Tools\Hashtag\CreateHashtagTool;
|
||||||
|
use App\Mcp\Tools\Hashtag\DeleteHashtagTool;
|
||||||
|
use App\Mcp\Tools\Hashtag\ListHashtagsTool;
|
||||||
|
use App\Mcp\Tools\Hashtag\UpdateHashtagTool;
|
||||||
|
use App\Mcp\Tools\Label\CreateLabelTool;
|
||||||
|
use App\Mcp\Tools\Label\DeleteLabelTool;
|
||||||
|
use App\Mcp\Tools\Label\ListLabelsTool;
|
||||||
|
use App\Mcp\Tools\Label\UpdateLabelTool;
|
||||||
|
use App\Mcp\Tools\Post\CreatePostTool;
|
||||||
|
use App\Mcp\Tools\Post\DeletePostTool;
|
||||||
|
use App\Mcp\Tools\Post\GetPostTool;
|
||||||
|
use App\Mcp\Tools\Post\ListPostsTool;
|
||||||
|
use App\Mcp\Tools\Workspace\GetWorkspaceTool;
|
||||||
|
use Laravel\Mcp\Server;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Instructions;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Name;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Version;
|
||||||
|
|
||||||
|
#[Name('TryPost')]
|
||||||
|
#[Version('1.0.0')]
|
||||||
|
#[Instructions('TryPost is a social media scheduling platform. Use this server to manage posts, hashtag groups, labels, workspaces, and API keys.')]
|
||||||
|
class TryPostServer extends Server
|
||||||
|
{
|
||||||
|
public int $defaultPaginationLength = 100;
|
||||||
|
|
||||||
|
protected array $tools = [
|
||||||
|
// Posts
|
||||||
|
ListPostsTool::class,
|
||||||
|
GetPostTool::class,
|
||||||
|
CreatePostTool::class,
|
||||||
|
DeletePostTool::class,
|
||||||
|
|
||||||
|
// Hashtags
|
||||||
|
ListHashtagsTool::class,
|
||||||
|
CreateHashtagTool::class,
|
||||||
|
UpdateHashtagTool::class,
|
||||||
|
DeleteHashtagTool::class,
|
||||||
|
|
||||||
|
// Labels
|
||||||
|
ListLabelsTool::class,
|
||||||
|
CreateLabelTool::class,
|
||||||
|
UpdateLabelTool::class,
|
||||||
|
DeleteLabelTool::class,
|
||||||
|
|
||||||
|
// Workspace
|
||||||
|
GetWorkspaceTool::class,
|
||||||
|
|
||||||
|
// API Keys
|
||||||
|
ListApiKeysTool::class,
|
||||||
|
CreateApiKeyTool::class,
|
||||||
|
DeleteApiKeyTool::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $resources = [];
|
||||||
|
|
||||||
|
protected array $prompts = [];
|
||||||
|
}
|
||||||
40
app/Mcp/Tools/ApiKey/CreateApiKeyTool.php
Normal file
40
app/Mcp/Tools/ApiKey/CreateApiKeyTool.php
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\ApiKey;
|
||||||
|
|
||||||
|
use App\Actions\ApiKey\CreateApiKey;
|
||||||
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
|
#[Description('Create a new API key. Returns the plain text token which is only shown once.')]
|
||||||
|
class CreateApiKeyTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$validated = $request->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.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Mcp/Tools/ApiKey/DeleteApiKeyTool.php
Normal file
35
app/Mcp/Tools/ApiKey/DeleteApiKeyTool.php
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\ApiKey;
|
||||||
|
|
||||||
|
use App\Actions\ApiKey\DeleteApiKey;
|
||||||
|
use App\Models\ApiToken;
|
||||||
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
|
#[Description('Delete an API key by ID.')]
|
||||||
|
class DeleteApiKeyTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$apiToken = ApiToken::where('workspace_id', $request->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.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Mcp/Tools/ApiKey/ListApiKeysTool.php
Normal file
24
app/Mcp/Tools/ApiKey/ListApiKeysTool.php
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\ApiKey;
|
||||||
|
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||||
|
|
||||||
|
#[IsReadOnly]
|
||||||
|
#[Description('List all API keys for the current workspace.')]
|
||||||
|
class ListApiKeysTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$tokens = $request->user()->currentWorkspace->apiTokens()->latest()->get();
|
||||||
|
|
||||||
|
return Response::structured($tokens->toArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Mcp/Tools/Hashtag/CreateHashtagTool.php
Normal file
37
app/Mcp/Tools/Hashtag/CreateHashtagTool.php
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Hashtag;
|
||||||
|
|
||||||
|
use App\Actions\Hashtag\CreateHashtag;
|
||||||
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
|
#[Description('Create a new hashtag group with a name and hashtag string.')]
|
||||||
|
class CreateHashtagTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$validated = $request->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").'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Mcp/Tools/Hashtag/DeleteHashtagTool.php
Normal file
35
app/Mcp/Tools/Hashtag/DeleteHashtagTool.php
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Hashtag;
|
||||||
|
|
||||||
|
use App\Actions\Hashtag\DeleteHashtag;
|
||||||
|
use App\Models\WorkspaceHashtag;
|
||||||
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
|
#[Description('Delete a hashtag group by ID.')]
|
||||||
|
class DeleteHashtagTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$hashtag = WorkspaceHashtag::where('workspace_id', $request->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.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Mcp/Tools/Hashtag/ListHashtagsTool.php
Normal file
24
app/Mcp/Tools/Hashtag/ListHashtagsTool.php
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Hashtag;
|
||||||
|
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||||
|
|
||||||
|
#[IsReadOnly]
|
||||||
|
#[Description('List all hashtag groups for the current workspace.')]
|
||||||
|
class ListHashtagsTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$hashtags = $request->user()->currentWorkspace->hashtags()->latest()->get();
|
||||||
|
|
||||||
|
return Response::structured($hashtags->toArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Mcp/Tools/Hashtag/UpdateHashtagTool.php
Normal file
43
app/Mcp/Tools/Hashtag/UpdateHashtagTool.php
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Hashtag;
|
||||||
|
|
||||||
|
use App\Actions\Hashtag\UpdateHashtag;
|
||||||
|
use App\Models\WorkspaceHashtag;
|
||||||
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
|
#[Description('Update a hashtag group name or hashtags.')]
|
||||||
|
class UpdateHashtagTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$validated = $request->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.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Mcp/Tools/Label/CreateLabelTool.php
Normal file
37
app/Mcp/Tools/Label/CreateLabelTool.php
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Label;
|
||||||
|
|
||||||
|
use App\Actions\Label\CreateLabel;
|
||||||
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
|
#[Description('Create a new label with a name and hex color.')]
|
||||||
|
class CreateLabelTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$validated = $request->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).'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Mcp/Tools/Label/DeleteLabelTool.php
Normal file
35
app/Mcp/Tools/Label/DeleteLabelTool.php
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Label;
|
||||||
|
|
||||||
|
use App\Actions\Label\DeleteLabel;
|
||||||
|
use App\Models\WorkspaceLabel;
|
||||||
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
|
#[Description('Delete a label by ID.')]
|
||||||
|
class DeleteLabelTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$label = WorkspaceLabel::where('workspace_id', $request->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.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Mcp/Tools/Label/ListLabelsTool.php
Normal file
24
app/Mcp/Tools/Label/ListLabelsTool.php
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Label;
|
||||||
|
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||||
|
|
||||||
|
#[IsReadOnly]
|
||||||
|
#[Description('List all labels for the current workspace.')]
|
||||||
|
class ListLabelsTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$labels = $request->user()->currentWorkspace->labels()->latest()->get();
|
||||||
|
|
||||||
|
return Response::structured($labels->toArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Mcp/Tools/Label/UpdateLabelTool.php
Normal file
43
app/Mcp/Tools/Label/UpdateLabelTool.php
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Label;
|
||||||
|
|
||||||
|
use App\Actions\Label\UpdateLabel;
|
||||||
|
use App\Models\WorkspaceLabel;
|
||||||
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
|
#[Description('Update a label name or color.')]
|
||||||
|
class UpdateLabelTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$validated = $request->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).'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/Mcp/Tools/Post/CreatePostTool.php
Normal file
33
app/Mcp/Tools/Post/CreatePostTool.php
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Post;
|
||||||
|
|
||||||
|
use App\Actions\Post\CreatePost;
|
||||||
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
|
#[Description('Create a new draft post in the current workspace. A post platform entry is created for each connected social account.')]
|
||||||
|
class CreatePostTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$workspace = $request->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.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Mcp/Tools/Post/DeletePostTool.php
Normal file
35
app/Mcp/Tools/Post/DeletePostTool.php
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Post;
|
||||||
|
|
||||||
|
use App\Actions\Post\DeletePost;
|
||||||
|
use App\Models\Post;
|
||||||
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
|
||||||
|
#[Description('Delete a post by ID.')]
|
||||||
|
class DeletePostTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$post = Post::where('workspace_id', $request->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.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Mcp/Tools/Post/GetPostTool.php
Normal file
35
app/Mcp/Tools/Post/GetPostTool.php
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Post;
|
||||||
|
|
||||||
|
use App\Models\Post;
|
||||||
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||||
|
|
||||||
|
#[IsReadOnly]
|
||||||
|
#[Description('Get a specific post by ID with all its platform content and labels.')]
|
||||||
|
class GetPostTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$post = Post::where('workspace_id', $request->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.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Mcp/Tools/Post/ListPostsTool.php
Normal file
28
app/Mcp/Tools/Post/ListPostsTool.php
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Post;
|
||||||
|
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||||
|
|
||||||
|
#[IsReadOnly]
|
||||||
|
#[Description('List all posts for the current workspace. Returns posts with their platforms, status, and scheduled date.')]
|
||||||
|
class ListPostsTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
$posts = $request->user()->currentWorkspace
|
||||||
|
->posts()
|
||||||
|
->with(['postPlatforms.socialAccount', 'labels'])
|
||||||
|
->latest('scheduled_at')
|
||||||
|
->paginate(50);
|
||||||
|
|
||||||
|
return Response::structured($posts->toArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Mcp/Tools/Workspace/GetWorkspaceTool.php
Normal file
22
app/Mcp/Tools/Workspace/GetWorkspaceTool.php
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Tools\Workspace;
|
||||||
|
|
||||||
|
use Laravel\Mcp\Request;
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\ResponseFactory;
|
||||||
|
use Laravel\Mcp\Server\Attributes\Description;
|
||||||
|
use Laravel\Mcp\Server\Tool;
|
||||||
|
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
|
||||||
|
|
||||||
|
#[IsReadOnly]
|
||||||
|
#[Description('Get the current workspace details including name and timezone.')]
|
||||||
|
class GetWorkspaceTool extends Tool
|
||||||
|
{
|
||||||
|
public function handle(Request $request): ResponseFactory
|
||||||
|
{
|
||||||
|
return Response::structured($request->user()->currentWorkspace->toArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
use Illuminate\Auth\Notifications\ResetPassword;
|
use Illuminate\Auth\Notifications\ResetPassword;
|
||||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||||
use Illuminate\Cache\RateLimiting\Limit;
|
use Illuminate\Cache\RateLimiting\Limit;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
@ -49,7 +50,10 @@ class AppServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function register(): void
|
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
|
// Disable wrapping of JSON resources
|
||||||
JsonResource::withoutWrapping();
|
JsonResource::withoutWrapping();
|
||||||
|
Model::shouldBeStrict(! $this->app->isProduction());
|
||||||
|
|
||||||
DB::prohibitDestructiveCommands(
|
DB::prohibitDestructiveCommands(
|
||||||
app()->isProduction(),
|
app()->isProduction(),
|
||||||
|
|
|
||||||
65
app/Providers/TelescopeServiceProvider.php
Normal file
65
app/Providers/TelescopeServiceProvider.php
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Laravel\Telescope\IncomingEntry;
|
||||||
|
use Laravel\Telescope\Telescope;
|
||||||
|
use Laravel\Telescope\TelescopeApplicationServiceProvider;
|
||||||
|
|
||||||
|
class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
// Telescope::night();
|
||||||
|
|
||||||
|
$this->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, [
|
||||||
|
//
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
"cashier-stripe-development",
|
"cashier-stripe-development",
|
||||||
"laravel-best-practices",
|
"laravel-best-practices",
|
||||||
"configuring-horizon",
|
"configuring-horizon",
|
||||||
|
"mcp-development",
|
||||||
"socialite-development",
|
"socialite-development",
|
||||||
"wayfinder-development",
|
"wayfinder-development",
|
||||||
"pest-testing",
|
"pest-testing",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
use App\Http\Middleware\EnsureSubscribed;
|
use App\Http\Middleware\EnsureSubscribed;
|
||||||
use App\Http\Middleware\HandleAppearance;
|
use App\Http\Middleware\HandleAppearance;
|
||||||
use App\Http\Middleware\HandleInertiaRequests;
|
use App\Http\Middleware\HandleInertiaRequests;
|
||||||
|
use App\Http\Middleware\Mcp\AuthenticateMcpToken;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
|
@ -35,6 +36,7 @@
|
||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
'subscribed' => EnsureSubscribed::class,
|
'subscribed' => EnsureSubscribed::class,
|
||||||
'api.auth' => AuthenticateApiToken::class,
|
'api.auth' => AuthenticateApiToken::class,
|
||||||
|
'mcp.auth' => AuthenticateMcpToken::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$middleware->preventRequestForgery(except: [
|
$middleware->preventRequestForgery(except: [
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@
|
||||||
|
|
||||||
use App\Providers\AppServiceProvider;
|
use App\Providers\AppServiceProvider;
|
||||||
use App\Providers\HorizonServiceProvider;
|
use App\Providers\HorizonServiceProvider;
|
||||||
|
use App\Providers\TelescopeServiceProvider;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
AppServiceProvider::class,
|
AppServiceProvider::class,
|
||||||
HorizonServiceProvider::class,
|
HorizonServiceProvider::class,
|
||||||
|
TelescopeServiceProvider::class,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,12 @@
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"inertiajs/inertia-laravel": "^3.0",
|
"inertiajs/inertia-laravel": "^3.0",
|
||||||
|
"laravel/ai": "^0.4.2",
|
||||||
"laravel/boost": "^2.0",
|
"laravel/boost": "^2.0",
|
||||||
"laravel/cashier": "^16.2",
|
"laravel/cashier": "^16.2",
|
||||||
"laravel/framework": "^13.0",
|
"laravel/framework": "^13.0",
|
||||||
"laravel/horizon": "^5.42",
|
"laravel/horizon": "^5.42",
|
||||||
|
"laravel/mcp": "^0.6.4",
|
||||||
"laravel/nightwatch": "^1.22",
|
"laravel/nightwatch": "^1.22",
|
||||||
"laravel/reverb": "^1.0",
|
"laravel/reverb": "^1.0",
|
||||||
"laravel/socialite": "^5.24",
|
"laravel/socialite": "^5.24",
|
||||||
|
|
@ -59,6 +61,7 @@
|
||||||
"laravel/pail": "^1.2.2",
|
"laravel/pail": "^1.2.2",
|
||||||
"laravel/pint": "^1.24",
|
"laravel/pint": "^1.24",
|
||||||
"laravel/sail": "*",
|
"laravel/sail": "*",
|
||||||
|
"laravel/telescope": "^5.19",
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"nunomaduro/collision": "^8.6",
|
"nunomaduro/collision": "^8.6",
|
||||||
"pestphp/pest": "^4.4",
|
"pestphp/pest": "^4.4",
|
||||||
|
|
|
||||||
216
composer.lock
generated
216
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "222ac114e81a021efda803dd571fb2d2",
|
"content-hash": "5408bdeb64bc5066a115e08ba7f378f3",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-crt-php",
|
"name": "aws/aws-crt-php",
|
||||||
|
|
@ -1518,6 +1518,72 @@
|
||||||
},
|
},
|
||||||
"time": "2026-03-25T21:07:46+00:00"
|
"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",
|
"name": "laravel/boost",
|
||||||
"version": "v2.4.1",
|
"version": "v2.4.1",
|
||||||
|
|
@ -4531,6 +4597,85 @@
|
||||||
],
|
],
|
||||||
"time": "2026-03-09T20:33:04+00:00"
|
"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",
|
"name": "psr/clock",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|
@ -9984,6 +10129,75 @@
|
||||||
},
|
},
|
||||||
"time": "2026-03-23T15:56:34+00:00"
|
"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",
|
"name": "mockery/mockery",
|
||||||
"version": "1.6.12",
|
"version": "1.6.12",
|
||||||
|
|
|
||||||
130
config/ai.php
Normal file
130
config/ai.php
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default AI Provider Names
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which of the AI providers below should be the
|
||||||
|
| default for AI operations when no explicit provider is provided
|
||||||
|
| for the operation. This should be any provider defined below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => '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'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
212
config/telescope.php
Normal file
212
config/telescope.php
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Laravel\Telescope\Http\Middleware\Authorize;
|
||||||
|
use Laravel\Telescope\Watchers;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Telescope Master Switch
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option may be used to disable all Telescope watchers regardless
|
||||||
|
| of their individual configuration, which simply provides a single
|
||||||
|
| and convenient way to enable or disable Telescope data storage.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'enabled' => 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),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Laravel\Ai\Migrations\AiMigration;
|
||||||
|
|
||||||
|
return new class extends AiMigration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('agent_conversations', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the migration connection name.
|
||||||
|
*/
|
||||||
|
public function getConnection(): ?string
|
||||||
|
{
|
||||||
|
return config('telescope.storage.database.connection');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$schema = Schema::connection($this->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -2,13 +2,16 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Mcp\Servers\TryPostServer;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Laravel\Mcp\Facades\Mcp;
|
||||||
|
|
||||||
Route::group(
|
Route::group(
|
||||||
[
|
[
|
||||||
'domain' => 'mcp.'.parse_url(config('app.url'), PHP_URL_HOST),
|
'domain' => 'mcp.'.parse_url(config('app.url'), PHP_URL_HOST),
|
||||||
],
|
],
|
||||||
function () {
|
function () {
|
||||||
//
|
Mcp::web('/trypost', TryPostServer::class)
|
||||||
|
->middleware('mcp.auth');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
20
stubs/agent-middleware.stub
Normal file
20
stubs/agent-middleware.stub
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace {{ namespace }};
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Laravel\Ai\Prompts\AgentPrompt;
|
||||||
|
use Laravel\Ai\Responses\AgentResponse;
|
||||||
|
|
||||||
|
class {{ class }}
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the incoming prompt.
|
||||||
|
*/
|
||||||
|
public function handle(AgentPrompt $prompt, Closure $next)
|
||||||
|
{
|
||||||
|
return $next($prompt)->then(function (AgentResponse $response) {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
44
stubs/agent.stub
Normal file
44
stubs/agent.stub
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace {{ namespace }};
|
||||||
|
|
||||||
|
use Laravel\Ai\Contracts\Agent;
|
||||||
|
use Laravel\Ai\Contracts\Conversational;
|
||||||
|
use Laravel\Ai\Contracts\HasTools;
|
||||||
|
use Laravel\Ai\Contracts\Tool;
|
||||||
|
use Laravel\Ai\Messages\Message;
|
||||||
|
use Laravel\Ai\Promptable;
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
|
class {{ class }} implements Agent, Conversational, HasTools
|
||||||
|
{
|
||||||
|
use Promptable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the instructions that the agent should follow.
|
||||||
|
*/
|
||||||
|
public function instructions(): Stringable|string
|
||||||
|
{
|
||||||
|
return 'You are a helpful assistant.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of messages comprising the conversation so far.
|
||||||
|
*
|
||||||
|
* @return Message[]
|
||||||
|
*/
|
||||||
|
public function messages(): iterable
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tools available to the agent.
|
||||||
|
*
|
||||||
|
* @return Tool[]
|
||||||
|
*/
|
||||||
|
public function tools(): iterable
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
56
stubs/structured-agent.stub
Normal file
56
stubs/structured-agent.stub
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace {{ namespace }};
|
||||||
|
|
||||||
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
|
use Laravel\Ai\Contracts\Agent;
|
||||||
|
use Laravel\Ai\Contracts\Conversational;
|
||||||
|
use Laravel\Ai\Contracts\HasStructuredOutput;
|
||||||
|
use Laravel\Ai\Contracts\HasTools;
|
||||||
|
use Laravel\Ai\Contracts\Tool;
|
||||||
|
use Laravel\Ai\Messages\Message;
|
||||||
|
use Laravel\Ai\Promptable;
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
|
class {{ class }} implements Agent, Conversational, HasStructuredOutput, HasTools
|
||||||
|
{
|
||||||
|
use Promptable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the instructions that the agent should follow.
|
||||||
|
*/
|
||||||
|
public function instructions(): Stringable|string
|
||||||
|
{
|
||||||
|
return 'You are a helpful assistant.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of messages comprising the conversation so far.
|
||||||
|
*
|
||||||
|
* @return Message[]
|
||||||
|
*/
|
||||||
|
public function messages(): iterable
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tools available to the agent.
|
||||||
|
*
|
||||||
|
* @return Tool[]
|
||||||
|
*/
|
||||||
|
public function tools(): iterable
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the agent's structured output schema definition.
|
||||||
|
*/
|
||||||
|
public function schema(JsonSchema $schema): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'value' => $schema->string()->required(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
37
stubs/tool.stub
Normal file
37
stubs/tool.stub
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace {{ namespace }};
|
||||||
|
|
||||||
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
|
use Laravel\Ai\Contracts\Tool;
|
||||||
|
use Laravel\Ai\Tools\Request;
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
|
class {{ class }} implements Tool
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the description of the tool's purpose.
|
||||||
|
*/
|
||||||
|
public function description(): Stringable|string
|
||||||
|
{
|
||||||
|
return 'A description of the tool.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the tool.
|
||||||
|
*/
|
||||||
|
public function handle(Request $request): Stringable|string
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tool's schema definition.
|
||||||
|
*/
|
||||||
|
public function schema(JsonSchema $schema): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'value' => $schema->string()->required(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
104
tests/Feature/Api/PostApiTest.php
Normal file
104
tests/Feature/Api/PostApiTest.php
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Enums\Post\Status as PostStatus;
|
||||||
|
use App\Enums\SocialAccount\Platform;
|
||||||
|
use App\Models\ApiToken;
|
||||||
|
use App\Models\Post;
|
||||||
|
use App\Models\SocialAccount;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Workspace;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->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();
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue