How to use cipi/agent in Laravel — project MCP for debug and seeders
By Andrea Pollastri · Last updated: · free to read, no paywall
SSHing into production for a migrate:status, a failed-job peek or a staging seeder is slow, unshareable and useless to an AI assistant. The official Laravel package cipi/agent puts webhooks, health checks and — most useful — a project MCP inside the app, so Cursor can read logs, run Artisan and query the database over HTTPS.
Why an agent inside the app
Cipi already owns the server: Nginx, PHP, databases, SSL, zero-downtime deploys. That is infrastructure. A Laravel app still has its own world — Eloquent models, seeders, Horizon queues, daily log rotation, the current migration set. An AI that only sees the repo guesses. An AI that can call tools on the running app stops guessing.
That is what the project MCP is for. It lives at POST /cipi/mcp on the app domain, talks MCP 2024-11-05 over HTTPS, and exposes six tools scoped to that application. No root SSH. No shared deploy key in the IDE. The assistant asks for health, tails laravel.log, runs db:seed --class=RoleSeeder on staging, and checks the row count with db_query.
Health check and MCP work on any Laravel 12+ host. Webhook deploys and the full log set (nginx, php, worker, deploy) expect a Cipi-managed environment. The reference lives in the Cipi Agent docs.
Install cipi/agent
Requirements: PHP 8.3+ and Laravel 12 or 13. The service provider auto-discovers — leave config/app.php alone.
$ composer require cipi/agent
$ php artisan cipi:status # config + live DB connectivityOn a Cipi VPS, cipi app create already injects CIPI_APP_USER, CIPI_WEBHOOK_TOKEN and the deploy paths. You only enable the optional services you want. Off Cipi, publish the config if you need different defaults:
$ php artisan vendor:publish --tag=cipi-configCommit and push. The next deploy picks the package up. Nothing else to wire on the panel side.
What the package actually does
| Feature | Endpoint | When you use it |
|---|---|---|
| Webhook deploy | POST /cipi/webhook | GitHub / GitLab push writes .deploy-trigger; Deployer runs as the app user within a minute |
| Health check | GET /cipi/health | UptimeRobot / Grafana: app, database, cache, queue backlog, last commit |
| Project MCP | POST /cipi/mcp | Cursor / VS Code / Claude: health, logs, SQL, Artisan, deploy — this guide |
| DB anonymizer | POST /cipi/db | GDPR-safe dumps for local/QA, Faker transforms, signed 15-minute download |
Each feature has its own Bearer token and can be switched off independently. A disabled endpoint returns 404 — it does not exist, as far as a scanner is concerned.
Turn on the project MCP
The MCP server is off by default. Enable it, mint a dedicated token, then print the client snippets:
$ php artisan cipi:service mcp --enable
$ php artisan cipi:generate-token mcp
$ php artisan cipi:mcpcipi:mcp prints the six tools and ready-to-paste JSON for Cursor (native HTTP), VS Code / Copilot, and Claude Desktop (via mcp-remote). The token lands in .env as CIPI_MCP_TOKEN. Do not reuse the webhook secret.
Connect Cursor, VS Code or Claude
For Cursor, put this in ~/.cursor/mcp.json (or Settings → MCP). Replace the domain and the token from your .env:
{
"mcpServers": {
"cipi-myapp": {
"type": "http",
"url": "https://yourdomain.com/cipi/mcp",
"headers": {
"Authorization": "Bearer YOUR_CIPI_MCP_TOKEN"
}
}
}
}VS Code 1.102+ uses the same HTTP transport in .vscode/mcp.json under a servers key. Claude Desktop needs the mcp-remote stdio bridge — php artisan cipi:mcp prints that block too.
Name the server after the app user (cipi-myapp, cipi-staging). One Laravel app, one MCP. If you run staging and production, register two servers and say which one you mean.
Debug without SSH
This is the part that changes the daily rhythm. You stay in the IDE. The assistant talks to the live app.
health — is it even up?
Same payload as GET /cipi/health: Laravel version, APP_DEBUG, database name, cache, queue driver, pending jobs, last deploy commit. Ask: “Is production healthy? Any queue backlog?” If checks.app.debug is true on a public host, you just found a problem without opening .env.
logs — last errors, not the whole file
The logs tool reads the last N lines (default 50, max 500) and keeps stack traces intact. Filters:
type—laravel,nginx,php,worker,deploylevel—error,warning, … (Laravel only)search— case-insensitive keyword, e.g.PaymentFailedor a job class
Daily rotation (laravel-YYYY-MM-DD.log) is detected automatically. A useful first prompt after a 500: “Show the last 100 Laravel errors, then the matching nginx lines.”
db_query — look, don’t wreck
Read: SELECT, SHOW, DESCRIBE, EXPLAIN. Write: INSERT, UPDATE, DELETE. Blocked: DROP, TRUNCATE, GRANT, REVOKE, file I/O. Results come back as an ASCII table, capped at 100 rows — enough to confirm a seeder, not enough to dump the users table.
# after a RoleSeeder on staging
SELECT id, name, created_at FROM roles ORDER BY id;
# did today’s signups land?
SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE;Seeders, migrate and cache via artisan
The artisan tool is the reason this package earns a slot in a Laravel repo. It runs any Artisan command except the long-running / interactive ones (serve, tinker, queue:work, queue:listen, schedule:work, horizon, octane:start, reverb:start). Everything else — including seeders — is fair game.
Prompts that actually save a hop to SSH:
- “Run
migrate:statuson staging.” - “Seed roles only:
db:seed --class=RoleSeeder.” - “Load demo orders with
DemoDataSeeder, thenSELECT COUNT(*) FROM orders.” - “
queue:failed— any jobs stuck since the last deploy?” - “
cache:clearandoptimize:clearafter the config change.”
Do not point this at production and say “run the DatabaseSeeder”. migrate:fresh --seed is not in the blocked list — the package will execute it. Use named seeders, on staging, and confirm with db_query. Production data is not a playground just because the transport is MCP instead of SSH.
A safe staging pattern: one seeder class per fixture, idempotent where you can, called by name. The assistant runs the class, reads the table, and only then do you promote the same seeder through CI. That is the opposite of “dump production and pray”.
Pair it with the anonymizer when local needs volume without PII: anonymize a production-shaped dump, load it locally, keep MCP seeders for the small reference tables (roles, plans, feature flags) that change every sprint.
A daily loop you can copy
You: Staging healthy? Any pending jobs?
Agent: health → healthy, queue 0, debug false, commit a1b2c3d
You: Last Laravel errors, search "InvoiceJob"
Agent: logs type=laravel level=error search=InvoiceJob
→ 1 error, missing column invoices.paid_at
You: migrate:status. Is 2026_08_22_add_paid_at pending?
Agent: artisan migrate:status → yes, pending
You: After I deploy, seed InvoiceStatusSeeder only.
Agent: deploy → queued
artisan db:seed --class=InvoiceStatusSeeder
db_query SELECT id, name FROM invoice_statuses
→ 4 rowsNo SSH session. No copy-paste from storage/logs. The same conversation works for a teammate who has the MCP token and should never have root on the box — which is most of the team.
Project MCP vs panel API MCP
Cipi ships two MCP servers. Mixing them up is the usual first mistake.
Project MCP (cipi/agent) | Panel API MCP (cipi/api) | |
|---|---|---|
| Where | POST /cipi/mcp on the app domain | POST /mcp on the API vhost |
| Scope | One Laravel app: its DB, logs, Artisan, deploy flag | The whole server: apps, SSL, databases, PHP, workers |
| Tools | 6 — health, app_info, deploy, logs, db_query, artisan | 50+ — create apps, issue certs, edit .env, run as app user |
| Use it for | Debug, seeders, migrate:status, queue peek | Provision, SSL, list every database, server cockpit |
Keep both in Cursor if you like. Ask the project MCP about this app; ask the panel API MCP to create the next staging clone. The spec-driven AI guide covers the wider loop — this page is the in-app half.
Tokens, blocks and least privilege
- Dedicated token.
CIPI_MCP_TOKENis not the webhook secret and not the health token. If it leaks,php artisan cipi:generate-token mcpand restart. Anyone with the token can run Artisan and write SQL (within the 100-row cap). - Off means 404.
php artisan cipi:service mcp --disableorCIPI_MCP=false. Prefer this on production until you actually need the IDE connected. - Blocked Artisan:
serve,tinker,queue:work,queue:listen,schedule:work,horizon,octane:start,reverb:start. - Blocked SQL:
DROP,TRUNCATE,GRANT,REVOKE, file I/O. - HTTPS only, no SSH. Fine for a developer who should deploy and inspect without
sudo. Still a privileged channel — treat the token like a production password.
Install the package, then the server
cipi/agent is the Laravel companion. Cipi is the free, open-source deploy CLI that injects the env and runs Deployer when the webhook or the MCP deploy tool fires.
Frequently asked questions
Do I need SSH to use the project MCP?
No. Once the package is deployed and CIPI_MCP is on, the IDE talks to https://yourdomain.com/cipi/mcp with the Bearer token. That is the point for teammates who should never log in as root.
What is the difference between the agent MCP and the Cipi API MCP?
The agent MCP is inside one Laravel app (six tools: health, logs, SQL, Artisan, deploy). The panel API MCP manages the whole VPS — create apps, SSL, databases, PHP. Use the agent for debug and seeders; use the API to provision.
Can I run db:seed on production through MCP?
Technically yes — db:seed is not blocked. Practically, only run a named, reviewed seeder, and never migrate:fresh --seed on live data. Prefer staging, then confirm with db_query.
Which Artisan commands are blocked?
serve, tinker, queue:work, queue:listen, schedule:work, horizon, octane:start, reverb:start. Long-running and interactive processes do not belong on an HTTP tool call.
Does cipi/agent work without a Cipi server?
Health check and MCP yes, on any Laravel 12+ host. Webhook-triggered Deployer deploys and the extra log types expect the Cipi layout under /home/<app>/.
How do I rotate a leaked MCP token?
php artisan cipi:generate-token mcp, restart the app (or reload PHP-FPM / Octane), update ~/.cursor/mcp.json. The old token dies with the .env rewrite.