1. Go to this page and download the library: Download gtapps/laravel-agentic library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
gtapps / laravel-agentic example snippets
use Spatie\LaravelData\Data;
class RefundInvoiceInput extends Data
{
public function __construct(
public int $invoiceId,
public float $amount,
public string $reason = 'requested_by_customer',
) {}
}
use Gtapps\LaravelAgentic\Attributes\AgentAction;
use Gtapps\LaravelAgentic\Contracts\ActionContext;
use Gtapps\LaravelAgentic\Enums\Surface;
#[AgentAction(
name: 'refund-invoice',
description: 'Refund an invoice to the original payment method.', // written for the MODEL
needsApproval: true,
surfaces: [Surface::Mcp, Surface::AiTool, Surface::Http, Surface::Cli, Surface::Job], // define the ones you want, omit for all
)]
class RefundInvoice
{
// Shared by default; define authorizeMcp(), authorizeHttp(), etc. to replace it for one surface.
public function authorize(ActionContext $ctx, RefundInvoiceInput $input): bool
{
return $ctx->user()->can('refund', Invoice::find($input->invoiceId));
}
public function handle(RefundInvoiceInput $input, ActionContext $ctx): RefundResult
{
// business logic; $ctx->caller() ∈ {mcp, ai-tool, http, cli, job}
}
}
use Gtapps\LaravelAgentic\Surfaces\Mcp\AgenticServer;
use Laravel\Mcp\Facades\Mcp;
Mcp::web('/mcp', AgenticServer::class)->middleware(['auth:sanctum']);
#[AgentAction(..., needsApproval: BigRefundsNeedApproval::class)]
// class BigRefundsNeedApproval {
// public function __invoke(RefundInvoiceInput $input): bool {
// return $input->amount > 100;
// }
// }
$response = $agent->forUser($user)->prompt('Refund invoice 42');
if ($response->hasPendingApprovals()) {
// Returns null until every paused call has an answer, so poll (or
// re-enter on your approval event).
$decisions = Agentic::approvalDecisions($response->pendingApprovals, $user);
if ($decisions !== null) {
$response = $agent->continue($response->conversationId, $user)->prompt($decisions);
}
}
// routes/web.php, POST only. Never grant over GET: chat-app link
// preview prefetchers will auto-approve your links.
Route::post('/approvals/{id}', function (string $id, Request $request) {
$granted = app(ApprovalBroker::class)->decide(
$id, // $approval->id from ApprovalRequested
$request->input('token'), // timing-safe verified
approve: true,
decidedBy: $request->user()->email,
);
return $granted ? response()->noContent() : abort(410);
})->middleware(['auth', 'can:approve-agentic']);
#[AgentAction(name: 'refund-invoice', surfaces: [Surface::Mcp, Surface::Cli])]
class RefundInvoice
{
public function authorize(ActionContext $ctx, RefundInvoiceInput $input): bool
{
return Gate::forUser($ctx->user())->allows('refund', $input->invoiceId);
}
public function authorizeCli(): bool
{
return true; // local-trust surface: the process boundary is the auth line
}
}
public function authorizeMcp(RefundInvoiceInput $input): Response
{
return $input->amount <= 10_000
? Response::allow()
: Response::deny('Refunds over $10,000 must be run by a human via CLI.');
}
class ListInvoicesInput extends Data
{
public function __construct(
#[Present]
/** @var int[] */
public array $ids = [],
) {}
}
class ListInvoicesInput extends PaginatedInput
{
// add your own filters via a constructor; page/perPage still apply
}
public function handle(ListInvoicesInput $input): mixed
{
return InvoiceSummary::collect(
Invoice::query()->paginate($input->perPage, page: $input->page)
);
}
class StoreInvoiceInput extends Data
{
public function __construct(public int $invoiceId, public float $amount) {}
/** Reuse the REST rules as the single source of truth during migration. */
public static function rules(): array
{
return (new StoreInvoiceRequest)->rules();
}
}
public function perPage(int $default = 100, int $max = 250): int
{
return max(1, min($max, $this->integer('per_page', $default)));
}
public function handle(ListInvoicesInput $input): mixed
{
$perPage = max(1, min(250, $input->perPage));
// ...
}
public static function rules(): array
{
return [
'invoiceId' => ['
use Gtapps\LaravelAgentic\Facades\Agentic;
it('refunds after checkout', function () {
$fake = Agentic::fake();
app(CheckoutFlow::class)->cancelAndRefund($order);
$fake->assertRan('refund-invoice', fn (array $args) => $args['invoiceId'] === $order->invoice_id);
$fake->assertAudited('refund-invoice');
});
it('never runs actions on validation failure', function () {
$fake = Agentic::fake();
// ...
$fake->assertNothingRan();
});
it('knocks for large refunds', function () {
$fake = Agentic::fake()->
return [
'discovery' => ['paths' => [app_path('Actions')]], // scanned for #[AgentAction]
'http' => [
'enabled' => env('AGENTIC_HTTP_ENABLED', false), // opt-in; set true and add your guard first
'prefix' => 'agentic', // POST /agentic/actions/{name}
'middleware' => ['api'], // add your guard, e.g. 'auth:sanctum'
],
'mcp' => [
'tiers' => ['unauthenticated' => []], // allowlist for anonymous callers
'exclude' => [], // hard denylist, beats everything
],
'approvals' => [
'ttl' => env('AGENTIC_APPROVALS_TTL', 600), // seconds until knock/grant expires to deny
'connection' => env('AGENTIC_APPROVALS_CONNECTION'), // null = default connection
],
'redact' => [], // dot-path globs, e.g. '*.password'
'audit' => [
'enabled' => env('AGENTIC_AUDIT_ENABLED', true),
'connection' => env('AGENTIC_AUDIT_CONNECTION'), // null = default connection
],
];
bash
php artisan vendor:publish --tag=agentic-config
php artisan vendor:publish --tag=agentic-agents-md # AGENTS.md for your repo