joshcirre/instruckt-laravel
Laravel package that adds visual feedback for AI coding agents: browser annotations become structured markdown, with a backend API, MCP tools, JSON storage, and a Blade toolbar. Installs via Vite plugin or Blade component. Requires PHP 8.2+ and Laravel 11/12.
Install (dev only, never a production dependency):
composer require joshcirre/instruckt-laravel --dev
php artisan instruckt:install
The install command is not passive — it edits source files you keep under version control:
instruckt/vite into vite.config.* with server: falseimport 'virtual:instruckt' to your JS entry point.mcp.json, .cursor/mcp.json, and other detected agent configsinstruckt npm packageRun it on a clean working tree and read the diff before committing. If you don't have Vite, it falls back to the Blade component path (<x-instruckt-toolbar />) — you can also wire that manually in your layout before </body>.
Where to look first, in order:
| Location | Why |
|---|---|
config/instruckt.php |
Backend behavior: enabled, route_prefix, api_middleware, store, screenshot_disk, mcp_token |
storage/app/_instruckt/annotations.json |
The actual payload — inspect it to confirm the browser is talking to Laravel |
vite.config.js |
Toolbar visual config (colors, keys, position, theme, adapters) when using the Vite plugin |
skills/instruckt/SKILL.md (installed agent skill) |
Teaches the agent how to recognize pasted markdown and when to call instruckt.resolve |
First use case. Boot npm run dev + php artisan serve, hit a page with a layout you dislike, press A, click the offending element, type the feedback, submit. Two things happen: structured markdown lands on your clipboard, and a JSON annotation persists to disk with source_file / source_line / class_name enrichment. Paste into Claude Code or Cursor; the agent has the element selector, classes, text content, and exact PHP file:line — no "which component renders this heading?" round trip.
Requirements: PHP 8.2+, Laravel 11–13, laravel/mcp optional but required for the MCP loop.
Vite plugin (preferred). apply: 'serve' means instruckt is absent from production builds entirely — zero bytes shipped, enforced by the bundler rather than by config.
instruckt({
server: false, // Laravel owns the backend
endpoint: '/instruckt',
adapters: ['livewire', 'blade'], // install always appends 'blade'
mcp: true,
position: 'bottom-right',
})
Blade component (no JS build changes). Gated behind config('instruckt.enabled'), accepts theme, position, adapters, colors, keys as attributes. Useful for legacy apps, or for QA/staging boxes where you don't want the plugin in vite.config.js.
Mixing them is the most common self-inflicted confusion — see Gotchas.
Loop 1 — clipboard (always works, no MCP required). Annotate → markdown auto-copies → paste into the agent → agent fixes. This is the resilient path; it survives agents without MCP support, SSH sessions, and *.test domains where clipboard auto-copy is skipped.
Loop 2 — MCP (closes the loop). After the agent fixes things:
instruckt.get_all_pending → annotation IDsinstruckt.get_screenshot for anything flagged with a screenshotinstruckt.resolve per fixed annotation → markers vanish from the browser with no reloadTrain the habit: fix first, resolve second. Resolving without fixing silently erases the user's feedback.
Local stdio — the agent shells out to Artisan:
{ "mcpServers": { "instruckt": {
"command": "php", "args": ["artisan", "mcp:start", "instruckt"]
} } }
Remote/hosted SSE — protect it, because /instruckt/mcp is otherwise open:
INSTRUCKT_MCP_TOKEN=your-secret-token
{ "instruckt": {
"url": "https://your-app.com/instruckt/mcp",
"headers": { "Authorization": "Bearer your-secret-token" }
} }
Independent middleware control is the point of the api_middleware / mcp_middleware split — you can put auth on the SSE endpoint without touching the annotation CRUD routes.
Defaults are file-based and stateless — perfect for a solo dev, wrong for anything shared.
| Concern | Local default | Shared/hosted |
|---|---|---|
| Annotations | storage/app/_instruckt/annotations.json |
INSTRUCKT_STORE=database + vendor:publish --tag=instruckt-migrations + migrate |
| Screenshots | local disk | INSTRUCKT_SCREENSHOT_DISK=s3 (any configured disk) → served via temporary signed URLs, survives deploys |
A single JSON file has no concurrency story and no cross-instance visibility. Flip to the database store the moment two people or two app servers are involved.
Commit: config/instruckt.php, the vite.config.js change, the app.js import, and .mcp.json (so teammates inherit the MCP entry for free). Ignore: storage/app/_instruckt/ — annotations are ephemeral working state, and screenshots bloat the repo fast.
For a spike, a POC, or a branch you're about to ship:
php artisan instruckt:uninstall # reverses vite inject, virtual import, config, layout component,
# MCP entries, skill dirs, storage, npm package
--force, --keep-config, --keep-npm for the edge cases. Then composer remove joshcirre/instruckt-laravel --dev. The fact that this exists and enumerates its own artifacts is the strongest maturity signal in the package.
The installer is a codemod — review it. It pattern-matches vite.config.* and your JS entry point. Unusual config shapes (multiple plugins arrays, conditional configs, vite.config.ts with wrappers) can produce a diff that lints or formats badly. Run your formatter and npm run build immediately after install; a broken Vite config discovered three commits later is the worst failure mode here.
Toolbar config lives in the wrong place half the time. With the Vite plugin, colors, keys, position, theme come from vite.config.js. The published PHP config governs backend only (enabled, routes, middleware, store, MCP). Editing config/instruckt.php colors and wondering why the markers are still indigo is the single most common wasted hour.
Re-publish config on upgrade. v0.4.27 added api_middleware, mcp_middleware, mcp_token, store, and screenshot_disk. If you published earlier, your config is missing them. The legacy middleware key still works but is superseded by api_middleware — don't set both and expect api_middleware to lose.
enabled defaults to APP_ENV=local, full stop. Staging QA gets nothing unless you set INSTRUCKT_ENABLED=true, and when you do, you are also registering unauthenticated CRUD routes under /instruckt. Default api_middleware is api — that's rate limiting, not auth. Add auth (plus a gate) before flipping the flag anywhere reachable.
Clipboard silently no-ops on http://*.test. navigator.clipboard needs a secure context. Auto-copy on submit is skipped; use the toolbar's copy button, which has a fallback path. If a teammate reports "it doesn't copy," check the hostname before debugging the package.
Screenshots need frozen animations. Press F before C — otherwise you capture a mid-transition frame and the agent reasons about a layout that never exists at rest. Also: instruckt.get_screenshot now returns a proper MCP error (isError: true) when a screenshot is missing. Trust that signal; don't let the agent go spelunking in storage/ for the file.
Don't abort() inside MCP tools. The 0.4.20 fix is instructive if you write custom tools against this store: abort() throws HttpException, which is uncatchable in the stdio context and kills the server. Store::updateAnnotation() returns null instead of aborting — follow that contract in extensions.
Version-pin against laravel/mcp. 0.4.30 relaxed the constraint to ^1.0 so it can coexist with Laravel MCP 1.0 and laravel/ai 1.x. On older instruckt releases you'll hit a Composer conflict the moment you add either. Pin a minor and read the release notes before bumping.
X clears only the current page. Markers are keyed by URL. Clearing /dashboard leaves /settings annotations live, and the agent's get_all_pending will still hand them back. Resolve via MCP rather than clearing, so the agent's list and the browser stay in sync.
Blade path in production is one env var away from shipping. With the Vite plugin, apply: 'serve' makes production leakage structurally impossible. With <x-instruckt-toolbar />, safety is config('instruckt.enabled') — a single INSTRUCKT_ENABLED=true in a prod .env loads the IIFE. If you go the Blade route, add a deploy-time assertion.
Extension points worth knowing: the tools config array (v0.4.22) hides individual toolbar buttons — strip screenshot or freeze for less technical stakeholders. SourceResolver (Livewire ReflectionClass → class + line, Blade view name → path) and the TrackBladeViews middleware are where the file:line context comes from; the POST /instruckt/resolve-source endpoint exposes the same resolution on demand if you build custom tooling. Remember that render_line and source_line can differ — render_line is where the element rendered, source_line is the component entry point.
Set expectations for maturity. 176 stars, zero dependents, a rapid release cadence (three releases in the 0.4.2x line touching storage, auth, and MCP transport). That's healthy velocity but pre-1.0 churn. Keep it --dev, pin a version, and run instruckt:uninstall before any branch that ships.
How can I help you explore Laravel packages today?