packstub/agents
| Install | |
|---|---|
composer require packstub/agents |
|
| Latest Version: | v1.9.0 |
| PHP: | ^8.4 |
| License: | MIT |
| Last Updated: | Oct 7, 2026 |
| Links: | GitHub · Packagist |
Agents for Laravel
An AI agent and an MCP server for your Laravel app, built on laravel/ai and laravel/mcp. Write a tool once and it serves both your own assistant and Claude Code, Claude Desktop, Cursor or any other MCP client, with your app's own authorization deciding who may run it. Filament Agents puts a chat and the operator pages on top of it inside a Filament panel.
Features
- One tool list, two front doors: your own agent and any MCP client (Claude Code, Cursor) call the same tools.
- Your app's authorization: the agent never does more than the signed-in person could, and a token narrows it further.
- Writes are proposals: a write waits for the person's approval, asked as a plain question.
- Turns that survive the request: queued, recorded and polled, with a sync driver when there is no worker.
- Knowledge base and web search: answers from your own documents, cited, and from public pages within an allow-list.
- Guard rails you switch on: a prompt guard against injections, and redaction of secrets in answers.
- A bounded bill: answer and token limits per user and per workspace, checked before the provider is called.
- Your assistant, your prompt: a scaffolded agent class on Anthropic, OpenAI, Gemini or xAI, with failover.
- Tenancy-aware: workspace-bound tokens, tenant databases and per-workspace keys, or no tenancy at all.
- Translatable: German, Spanish, Romanian and Russian included.
Compatibility
| Package | Laravel | PHP | laravel/ai | laravel/mcp |
|---|---|---|---|---|
| 1.x | 13.x | 8.4+ | ^0.11 | ^0.9 |
Installation
composer require packstub/agents
php artisan packstub-agents:install
The install command publishes the config, offers to run the migrations and scaffolds app/Ai/Agents/Assistant.php. Register the agent and the tools in a service provider and put a provider key in .env (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY or XAI_API_KEY, with AGENT_PROVIDER=anthropic|openai|gemini|xai):
use Packstub\Agents\Facades\Agents;
public function boot(): void
{
Agents::useAgent(\App\Ai\Agents\Assistant::class);
Agents::useServer(\App\Mcp\Servers\AcmeServer::class);
Agents::authorizeUsing(fn (string $ability) => auth()->user()->can($ability));
}
The MCP endpoint answers without a key, since it does not need a model. Read more: Installation.
In a Filament panel
packstub/filament-agents requires this package and adds what a panel shows: the chat pages with an "Ask …" button, approve-in-chat writes, show-table rendering a resource's own table under an answer, the Agent access page that mints tokens, and the operator's AI limits and AI turns pages. Install it instead of this package in a panel app; everything on this page applies unchanged, registered through AgentsPlugin instead of the facade. Read more: Filament Agents.
Writing tools
php artisan packstub-agents:tool SearchOrders --ability=orders.view
php artisan packstub-agents:tool ConfirmOrder --write --ability=orders.manage
A tool extends Packstub\Agents\Mcp\AgentTool, declares its $ability, and implements run(Request): array and schema(JsonSchema): array. Mark reads with #[IsReadOnly]; anything else is approval-gated for the agent and needs a write token over MCP. Domain exceptions come back to the model as tool errors, never to the person as a crash.
#[IsReadOnly]
#[Description('Find orders by number, customer, status and date.')]
class SearchOrders extends AgentTool
{
protected ?string $ability = 'orders.view';
protected function run(Request $request): array
{
$filters = AgentResources::normalizeFilters('orders', (array) $request->get('filters'));
$query = AgentResources::apply('orders', Order::query(), $filters);
return [
'total' => $query->count(),
'rows' => $query->limit($this->limit($request))->get()->map(fn (Order $o) => Orders::agentSummary($o))->all(),
];
}
public function schema(JsonSchema $schema): array
{
return [
'filters' => $schema->object(AgentResources::filterSchema($schema, 'orders')),
'limit' => $schema->integer(),
];
}
}
List the tools on the server class, reads first. The agent reads the same list:
class AcmeServer extends \Packstub\Agents\Mcp\AgentServer
{
protected string $name = 'Acme';
protected string $instructions = 'The back office of an online shop. Start with search-orders.';
protected array $tools = [
Tools\SearchOrders::class,
\Packstub\Agents\Mcp\Tools\DrawChart::class,
Tools\ConfirmOrder::class,
];
}
Agents::useTools([...]) works instead of a server class. A write tool may add describe(array $arguments): ?string to phrase its own proposals ("Confirm order RO-00016 for Acme?"); without one the question is the tool's title and its first argument. Read more: Tools.
The agent
packstub-agents:agent scaffolds App\Ai\Agents\Assistant, a subclass of Packstub\Agents\Ai\Agent with two slots to fill: persona() (who it is) and domain() (what the workspace is). The base class supplies the generic working and answering rules, the dynamic context (date, workspace, person, role, language — sent with the question, so the system prompt and the history stay cacheable) and the provider options (Anthropic cache breakpoints on the instructions and the settled history, reasoning effort or thinking level per model). Append to any of them by overriding workRules(), answerRules() or context() and merging the parent's list; suggestions() gives an empty chat its starter questions.
class Assistant extends Agent
{
protected function persona(): string
{
return 'You are Ask Acme, the back-office assistant of an online shop.';
}
protected function domain(): string
{
return <<<'PROMPT'
- Orders move from placed to paid to shipped; a cancelled order keeps its number.
- Warehouse staff may confirm and ship; only managers may refund.
PROMPT;
}
}
It runs on Anthropic, OpenAI, Gemini or xAI with a model catalog (Claude Opus 5, Claude Haiku 4.5, Claude Opus 5 · Deep); any other laravel/ai provider, Ollama included, runs on its smartest and cheapest models. A failover list (AGENT_FAILOVER=gemini,openai) keeps answering when a provider is overloaded.
Read more: The agent.
Running a turn
Start a conversation, queue a turn, poll it. The RunAgentTurn job restores who asked and where (the person, the guard, the workspace, the locale), runs the middleware pipeline with the budget check, streams the answer from the provider and records what it cost:
$conversation = app(AgentConversationStore::class)->startConversation($user, $question);
$turn = app(AgentTurns::class)->enqueue($conversation, $user, ['prompt' => $question], null, 'auto', null); // model key, page context ("orders/12")
AgentChat::for($user) wraps this for a chat surface of your own (send, decide, retry, stop, the transcript with its proposals, the context meter). GET agents/chat/{conversation}/turn returns the answer so far and a status line; run php artisan queue:work, or set AGENT_TURN_DRIVER=sync to run the job inside the request — a turn no worker takes within AGENT_WORKER_WAIT seconds says so on that status line. Long chats replay a token-budgeted window with a rolling summary. Read more: The agent.
Filters and charts
A class implementing AgentResource gives the model a filter vocabulary and a record summary, shared by every search tool that names the same key:
class Orders implements AgentResource
{
public static function agentKey(): string { return 'orders'; }
public static function agentSummary(Model $record, bool $full = false): array
{
return ['number' => $record->number, 'status' => $record->status, 'url' => route('orders.show', $record)];
}
public static function agentFilters(): array
{
return [
Filter::text('query')->description('Order number or customer.')
->apply(fn (Builder $q, string $t) => $q->where('number', 'like', "%{$t}%")),
Filter::enum('status', OrderStatus::class)->multiple()
->apply(fn (Builder $q, array $s) => $q->whereIn('status', $s)),
Filter::date('placed_from')
->apply(fn (Builder $q, string $d) => $q->where('placed_at', '>=', $d)),
];
}
}
Register it with Agents::useResources([Orders::class]). draw-chart renders bar, line, pie and doughnut charts from numbers the model already retrieved, and any tool can return a chart key of its own. Read more: Tables and charts.
MCP clients
Mint a Sanctum token with the abilities the client should have — read, write, tool:{name} to scope it, tenant:{slug} to bind it to a workspace — and connect any MCP client to POST /mcp:
$token = $user->createToken('laptop', ['read', 'tool:search-orders'], now()->addDays(30))->plainTextToken;
claude mcp add --transport http acme https://acme.test/mcp --header "Authorization: Bearer 3|…"
The endpoint sits behind throttle, auth:sanctum and the package's own middleware, so external agents get exactly the tools the person's role and their token allow. Read more: MCP clients.
Budgets and limits
config/packstub-agents.php holds the platform ceiling (AGENT_TURNS_PER_MINUTE, AGENT_TURNS_PER_DAY, AGENT_TOKENS_PER_DAY, AGENT_TOKENS_PER_MONTH, AGENT_USER_TOKENS_PER_DAY, AGENT_USER_TOKENS_PER_MONTH, AGENT_PROMPT_MAX_CHARS). Rows in agent_limits override it: one global row, optional rows per workspace and per user, empty fields inherit. AgentBudget::summary() gives the numbers for a settings page, and every ended turn keeps its record (provider, model, tokens, tools, duration, how it ended), optionally as one log line. Read more: Budgets and limits.
Tenancy
Tell the package what a workspace is and how the current one is found:
Agents::tenantModel(Team::class, slugAttribute: 'slug');
Agents::tenantUsing(fn () => auth()->user()?->currentTeam);
Agents::enteringTenant(function (Team $team) {
tenancy()->initialize($team);
return fn () => tenancy()->end();
});
Set the MCP path with the workspace in it ('mcp' => ['path' => 'mcp/{tenant}']) and the middleware looks the workspace up by its slug, checks the person's membership and the token's tenant:{slug} ability, and enters it before any tool runs. A workspace can bring its own provider key through credentialsUsing(). For a database-per-tenant app set 'run_migrations' => false, publish the migrations, keep create_agent_limits_table central and move the chat tables next to your tenant migrations. Read more: Tenancy.
Configuration
Agents::useAgent(Assistant::class); // your Agent subclass
Agents::useServer(AcmeServer::class); // the MCP server class with the tool list
Agents::useTools([...]); // or a plain tool list
Agents::addTools([...]); // tools appended to the server's own list
Agents::useResources([Orders::class]); // the AgentResource classes
Agents::useMiddleware([AuditTurns::class]); // your own agent middleware
Agents::authorizeUsing(fn (string $ability) => ...); // how an ability is checked for the current person
Agents::roleLabelUsing(fn () => ...); // the person's role, for the prompt and refusals
Agents::credentialsUsing(fn () => new WorkspaceCredentials(...)); // a workspace's own provider key
Agents::limitsAuthorizeUsing(fn () => ...); // who may edit the agent_limits rows
Agents::tenantUsing(fn () => ...); // the current workspace
Read more: Configuration.
Documentation
- Installation
- Tools
- The agent
- Tables and charts
- MCP clients
- Budgets and limits
- Tenancy
- Configuration
- Security
- Testing
Testing
composer test
In your own app, fake the model with Assistant::fake([...]) and drive tools through AcmeServer::tool(ToolClass::class, [...]). Never call a provider from tests. Read more: Testing.
Changelog
See the changelog.
Security vulnerabilities
This package lets a model act inside your app, so we take reports seriously. Please e-mail support@packstub.dev rather than opening a public issue. The threat model is documented on the Security page.
Credits
License
MIT. See the license file.
Related Packages
An in-panel AI assistant and an MCP server for Filament v5 panels, built on lara...
A beautiful AI chat widget plugin for Filament v3 with OpenAI integration
AI tooling for the Wire ecosystem – an MCP server, AI guidelines and agent skill...
Enterprise AI spend-governance for Laravel: cross-provider metering, budgets, po...
A drip-feed verification pipeline as an MCP server: phases, steps, and a cursor...
Version History
| Version | Released | PHP | Laravel | License |
|---|---|---|---|---|
| v1.9.0 | ^8.4 | ^13.0 | MIT | |
| v1.8.0 | ^8.4 | ^13.0 | MIT | |
| v1.7.2 | ^8.4 | ^13.0 | MIT | |
| v1.7.1 | ^8.4 | ^13.0 | MIT | |
| v1.7.0 | ^8.4 | ^13.0 | MIT | |
| v1.6.0 | ^8.4 | ^13.0 | MIT | |
| v1.5.0 | ^8.4 | ^13.0 | MIT | |
| v1.4.0 | ^8.4 | ^13.0 | MIT | |
| v1.3.0 | ^8.4 | ^13.0 | MIT | |
| v1.2.1 | ^8.4 | ^13.0 | MIT | |
| v1.2.0 | ^8.4 | ^13.0 | MIT | |
| v1.1.0 | ^8.4 | ^13.0 | MIT | |
| v1.0.0 | ^8.4 | ^13.0 | MIT |