Install
$ agentstack add mcp-vinkius-labs-laravel-vurb ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Turn any Laravel app into a production MCP Server. Zero TypeScript. PHP 8.2+ Attributes · Presenters that control what the LLM sees · PII Redaction · Eloquent Model Bridge · FSM State Gates · One command — every AI connects.
[](https://packagist.org/packages/vinkius-labs/laravel-vurb) [](https://www.php.net/) [](https://laravel.com/) [](https://modelcontextprotocol.io/) [](https://github.com/vinkius-labs/laravel-vurb/actions) [](https://github.com/vinkius-labs/laravel-vurb/actions) [](LICENSE) [](llms.txt) [](https://github.com/vinkius-labs/vurb.ts)
Vurb.ts Docs · [Quick Start](#quick-start) · [Architecture](#architecture) · [Testing](#testing--mva-assertions) · [llms.txt](llms.txt)
🤖 Try it right now — zero install: ▶ Open in Claude · ▶ Open in ChatGPT
Why Laravel Vurb?
Your Laravel app already has the business logic — Eloquent models, policies, middleware, jobs. Why rewrite it in TypeScript just to connect to an AI agent?
Laravel Vurb bridges PHP to the Model Context Protocol. Write a PHP class. Decorate with attributes. Run php artisan vurb:serve. Claude, Cursor, GitHub Copilot, Windsurf — every MCP-compatible client connects instantly.
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ AI Agent │ MCP │ Vurb.ts Daemon │ HTTP │ Laravel App │
│ (Cursor, Claude │◄─────►│ (auto-managed) │◄─────►│ (your tools) │
│ Copilot, etc.) │ stdio │ via npx │ JSON │ via bridge │
└─────────────────┘ └──────────────────┘ └──────────────────┘
No Node.js knowledge required. No daemon configuration. The package handles everything.
Zero Learning Curve — Ship a SKILL.md, Not a Tutorial
Every package you've adopted followed the same loop: read the docs, study the conventions, hit an edge case, search GitHub issues, re-read the docs. Weeks before your first PR. Your AI coding agent does the same — it hallucinates raw MCP SDK patterns or invents Laravel conventions because it has no formal contract to work from.
Laravel Vurb ships a SKILL.md — a machine-readable architectural contract that your AI agent ingests before writing a single line. Not a tutorial. Not a "getting started guide" the LLM will paraphrase loosely. A structural specification: every PHP Attribute, every VurbTool method, every Presenter composition rule, every middleware signature, every name-inference convention. The agent doesn't approximate — it compiles against the spec.
The agent reads SKILL.md and produces:
// app/Vurb/Presenters/PatientPresenter.php — generated by your AI agent
class PatientPresenter extends VurbPresenter
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'status' => $this->status,
'physician' => $this->attending_physician,
];
// ssn, diagnosis, internal_notes — physically absent from response
}
public function systemRules(): array
{
return [
'HIPAA: diagnosis visible in UI blocks but NEVER in conversation text.',
'Always confirm physician identity before discharge actions.',
];
}
public function suggestActions(): array
{
if ($this->status === 'cleared') {
return [['tool' => 'patients.discharge', 'reason' => 'Physician has signed off']];
}
return [['tool' => 'patients.sign_off', 'reason' => 'Awaiting physician sign-off']];
}
}
// app/Vurb/Tools/Patients/DischargePatient.php — generated by your AI agent
#[Description('Discharge a patient after physician sign-off')]
#[Instructions('NEVER call without verifying physician sign-off. Always confirm patient identity.')]
#[Presenter(PatientPresenter::class)]
#[FsmBind(states: ['cleared'], event: 'DISCHARGE')]
class DischargePatient extends VurbTool
{
public function verb(): string { return 'mutation'; }
public function handle(
#[Param(description: 'Patient ID', example: 'PAT-001')]
string $id,
): Patient {
$patient = Patient::findOrFail($id);
$patient->update(['status' => 'discharged', 'discharged_at' => now()]);
return $patient;
}
}
Correct Presenter with egress firewall — SSN and diagnosis physically stripped. FSM gating that makes patients.discharge invisible until physician sign-off. JIT system rules. Suggested actions computed from patient state. First pass — no corrections.
This works on Cursor, Claude Code, GitHub Copilot, Windsurf, Cline — any agent that can read a file. The SKILL.md is the single source of truth: the agent doesn't need to have been trained on Laravel Vurb, it just needs to read the spec.
> You don't learn Laravel Vurb. You don't teach your agent Laravel Vurb. You hand it a 400-line contract. It writes the server. You review the PR.
> 💡 The links above inject a super prompt that forces the AI to read [llms.txt](llms.txt) before writing code — guaranteeing correct MVA patterns, not hallucinated syntax.
When you install the package, the SKILL.md and llms.txt are automatically published to your project:
php artisan vurb:install
# → llms.txt copied to project root
# → .claude/skills/laravel-vurb-development/ created with SKILL.md + reference examples
You can also publish them individually:
php artisan vendor:publish --tag=vurb-llms # llms.txt → project root
php artisan vendor:publish --tag=vurb-skills # SKILL.md → .claude/skills/
Table of Contents
- [Zero Learning Curve — Ship a SKILL.md, Not a Tutorial](#zero-learning-curve--ship-a-skillmd-not-a-tutorial)
- [Quick Start](#quick-start)
- [How It Works — The Bridge Architecture](#how-it-works--the-bridge-architecture)
- [Writing Tools](#writing-tools)
- [Your First Tool](#your-first-tool)
- [Name Inference](#name-inference)
- [Semantic Verbs](#semantic-verbs)
- [PHP Attributes — Full Control](#php-attributes--full-control)
- [Dependency Injection in Handlers](#dependency-injection-in-handlers)
- [Presenters — Control What the LLM Sees](#presenters--control-what-the-llm-sees)
- [Routers — Group & Namespace Tools](#routers--group--namespace-tools)
- [Middleware](#middleware)
- [Eloquent Model Bridge](#eloquent-model-bridge)
- [FSM State Gate — Temporal Tool Governance](#fsm-state-gate--temporal-tool-governance)
- [DLP Redaction — PII Never Reaches the LLM](#dlp-redaction--pii-never-reaches-the-llm)
- [Governance & Lockfile](#governance--lockfile)
- [Observability — Telescope & Pulse](#observability--telescope--pulse)
- [Testing — MVA Assertions](#testing--mva-assertions)
- [Configuration Reference](#configuration-reference)
- [Artisan Commands](#artisan-commands)
- [Architecture](#architecture)
- [Ecosystem](#ecosystem)
- [Contributing](#contributing)
- [License](#license)
Quick Start
composer require vinkius-labs/laravel-vurb
php artisan vurb:install
The installer publishes config, creates app/Vurb/Tools/, installs the Node.js daemon, and generates a secure internal token.
Generate your first tool:
php artisan vurb:make-tool GetCustomerProfile --query
// app/Vurb/Tools/GetCustomerProfile.php
namespace App\Vurb\Tools;
use Vinkius\Vurb\Attributes\Param;
use Vinkius\Vurb\Tools\VurbTool;
class GetCustomerProfile extends VurbTool
{
public function description(): string
{
return 'Retrieve a customer profile by ID.';
}
public function verb(): string
{
return 'query';
}
public function handle(
#[Param(description: 'The customer ID', example: 42)]
int $id,
): array {
$customer = \App\Models\Customer::findOrFail($id);
return $customer->only(['id', 'name', 'plan', 'created_at']);
}
}
Start the server:
php artisan vurb:serve
That's it. Your Laravel app is now an MCP server. Connect any AI client.
How It Works — The Bridge Architecture
Laravel Vurb uses a thin daemon bridge — a lightweight Vurb.ts process that speaks MCP natively over stdio/HTTP. The daemon reads a compiled Schema Manifest from your PHP tool definitions and proxies every tool call back to Laravel over HTTP.
Schema Manifest (JSON)
┌───────────────────────┐
│ tools, presenters, │
php artisan vurb:serve │ models, FSM, state │ VURB_DAEMON_READY
│ │ sync, skills │ │
▼ └───────────┬───────────┘ ▼
┌──────────────┐ │ ┌──────────────┐
│ Laravel │ POST /_vurb/... │ │ Vurb.ts │
│ Bridge │◄───────────────────┤────────────│ Daemon │
│ Controller │ (X-Vurb-Token) │ │ (npx tsx) │
└──────────────┘ │ └──────────────┘
│ ▲
│ MCP │
│ │
┌─────┴──────┐ ┌───────┴──────┐
│ Manifest │ │ AI Client │
│ Compiler │ │ (Cursor, │
│ │ │ Claude, │
└─────────────┘ │ Copilot) │
└──────────────┘
Key design decisions:
- No Node.js knowledge required — the daemon is auto-installed and managed via
npx - Timing-safe token authentication — bridge endpoints are protected with
X-Vurb-Token - PHP reflection → JSON Schema — your typed
handle()parameters become the tool's input schema automatically - Zero config manifests — the compiler reads your tool classes, attributes, and router structure
Writing Tools
Your First Tool
Every tool extends VurbTool and implements handle():
use Vinkius\Vurb\Tools\VurbTool;
class ListOrders extends VurbTool
{
public function description(): string
{
return 'List recent orders for a customer.';
}
public function handle(int $customer_id, int $limit = 10): array
{
return Order::where('customer_id', $customer_id)
->latest()
->limit($limit)
->get()
->toArray();
}
}
That's it. The reflection engine reads your type hints:
int $customer_id→{ "type": "integer", "description": "customer_id" }(required)int $limit = 10→{ "type": "integer", "description": "limit" }(optional, default: 10)
Name Inference
Tool names are auto-inferred from the class name. No manual wiring:
| Class Name | Inferred Name | | :------------------- | :---------------------- | | GetCustomerProfile | customers.get_profile | | CreateInvoice | invoices.create | | ListOrders | orders.list | | SearchProducts | products.search | | ProcessPayment | payments.process | | SendNotification | notifications.send |
Override with public function name(): string { return 'my.custom_name'; }.
Semantic Verbs
Every tool declares its intent. The daemon uses this for MCP annotations:
public function verb(): string
{
return 'query'; // read-only, idempotent, cacheable
return 'mutation'; // writes data, destructive, invalidates cache
return 'action'; // side-effect (email, webhook), idempotent
}
PHP Attributes — Full Control
Decorate tools and parameters with attributes for precise schema generation:
use Vinkius\Vurb\Attributes\{Tool, Param, Description, Instructions, Tags, Presenter, Cached, Invalidates, FsmBind, Concurrency};
#[Description('Deep search across all customer records')]
#[Instructions('Only call when the user explicitly asks for a customer lookup. Never infer customer IDs.')]
#[Tags('crm', 'search')]
#[Cached(ttl: 120)]
#[Concurrency(max: 3)]
class SearchCustomers extends VurbTool
{
public function description(): string
{
return 'Search customers by name or email.';
}
public function verb(): string
{
return 'query';
}
public function handle(
#[Param(description: 'Search query — name, email, or phone', example: 'jane.doe@acme.com')]
string $query,
#[Param(description: 'Max results to return')]
int $limit = 20,
): array {
return Customer::search($query)->take($limit)->get()->toArray();
}
}
| Attribute | Target | Purpose | | :---------------- | :----------- | :----------------------------------------------------------- | | #[Tool] | Class | Override name(), description() | | #[Param] | Parameter | Description, example, enum items | | #[Description] | Class/Method | Override description string | | #[Instructions] | Class | Anti-hallucination instructions injected into LLM context | | #[Tags] | Class | Capability filtering tags | | #[Presenter] | Class | Link to Presenter class | | #[Cached] | Class | Cache tool results (optional TTL) | | #[Stale] | Class | Mark as ephemeral (always refetch) | | #[Invalidates] | Class | Mutation invalidation patterns ('customers.*') | | #[FsmBind] | Class | FSM state restriction (tool only visible in specific states) | | #[Concurrency] | Class | Max parallel executions | | #[AgentLimit] | Class | Rate limit per agent session | | #[Hidden] | Parameter | Exclude from LLM-visible schema
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: vinkius-labs
- Source: vinkius-labs/laravel-vurb
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.