feat(mcp): end-to-end agent-driven site bootstrap (project_new → setup)#240
Merged
feat(mcp): end-to-end agent-driven site bootstrap (project_new → setup)#240
Conversation
…hout agent hand-holding
Observed in a real Claude Code session: the agent scaffolded a Laravel
project via project_new, called site_link + env_setup, and the site
returned 500 on every request. It took three extra back-and-forth turns
(composer install → key:generate → touch database/database.sqlite →
artisan migrate) to get to "Laravel welcome page loads". Every step that
failed is something lerd already knew about; the failures were all
because the tools stopped short of the obvious next action.
Root causes:
1. Framework create commands use --no-install --no-plugins --no-scripts
(deliberate, to keep scaffold deterministic), so project_new returned
success with an empty vendor/. Agents have no reason to know this.
2. env_setup runs `lerd env`. For Laravel's default DB_CONNECTION=sqlite,
the DB-swap prompt is gated behind isInteractive(). Via MCP there is
no TTY, the prompt is skipped, and the sqlite-file-creation block
below (gated behind lerdYAMLServices["sqlite"]) never fires. Result:
no database/database.sqlite, Laravel 500s immediately.
3. The SKILL.md workflows didn't cover end-to-end bootstrap, cloned
project setup, or the 500-debug path, so agents had to improvise.
Fixes:
- env.go: non-interactive callers (MCP, scripts) default the sqlite
choice to "keep sqlite" instead of skipping. The choice is persisted
to .lerd.yaml and the existing sqlite-file-creation block runs, so
database/database.sqlite is touched and the site can respond. Agents
can still call `db_set` / `lerd db set` to switch to mysql/postgres
afterwards; nothing locks in sqlite beyond the default.
- server.go: execProjectNew now runs `composer install --no-interaction`
in the appropriate FPM container when composer.json is present but
vendor/ is missing. New runComposerInstallIfNeeded helper is
unit-tested against both no-composer.json and vendor-already-present
cases so the happy paths don't regress into re-pulling on re-runs.
- mcp.go SKILL: replace the two Laravel-specific workflows with three
framework-agnostic end-to-end flows:
* Bootstrap a new project from scratch (uses project_new, shows
artisan/console fork for the migration step, notes the new
composer-install-after-scaffold and DB-auto-create behaviour).
* Set up a cloned project (composer install BEFORE env_setup so
APP_KEY generation has vendor/, then migrate).
* Debugging a 500 — ordered checklist starting at logs(), through
env_check / which / composer install / APP_KEY / migrate /
service start / doctor as a last resort.
Bumped the claudeSkillContent size ceiling 41000 → 42000 to fit the
new workflows; any further growth still requires an explicit ceiling
bump so accretion remains visible in diffs.
Net effect: agents running `project_new` followed by `site_link` +
`env_setup` now get a working site in four tool calls, same four calls
that failed before. The clone workflow is symmetric. The debug flow
turns a "I'm stuck on a 500" session into a deterministic sequence.
…ion drifted IsMCPGloballyRegistered used `claude mcp list --scope user`, but newer Claude CLI rejects `--scope` on `list`, so the function always returned false. mcpEnabledGlobally still worked via the marker-file fallback, but refreshGlobalMCPSkills never had a signal to re-run `claude mcp add` after the skill files were rewritten. Result: after an uninstall+install cycle (or any Claude config migration that drops the lerd entry) users ended up with SKILL.md present but no lerd in `claude mcp list`. - IsMCPGloballyRegistered now calls `claude mcp get lerd` (exit 0 when registered, 1 otherwise). Guards with exec.LookPath so machines without the claude CLI return false immediately. - New ensureClaudeMCPRegistered runs the idempotent remove-then-add pattern (`claude mcp remove -s user lerd` + `claude mcp add -s user lerd -- lerd mcp`), same shape as RunMCPEnableGlobal. Prints a manual-run hint on failure. - refreshGlobalMCPSkills (invoked by `lerd install` and `lerd update`) calls ensureClaudeMCPRegistered when marker files say the user opted in globally but `claude mcp get lerd` fails. Self-healing on every reinstall; no-op when already registered or claude CLI is absent.
The CLI lerd setup is interactive — it shows a huh form where the user
toggles each framework setup step (migrate, storage:link, db:seed, etc.)
before running the selected ones. That works fine for humans but leaves
MCP agents with no way to trigger those steps: they can call artisan or
console manually, but only if they know exactly which commands the
framework's YAML declares as post-install bootstrap.
This was the missing link in the new-project flow:
project_new → site_link → env_setup → ???
→ artisan migrate? → artisan storage:link? → who knows
Agents would stop at env_setup (.env written, APP_KEY generated, DB
created / sqlite touched) and leave migrations unrun, so the site
loaded but had no tables.
The new setup MCP tool:
- Resolves path → site → framework (site_link prerequisite).
- Loops over fw.Setup and executes every entry where Default is true
and the Check rule (if any) passes in the project directory. Check
rules gate framework-optional steps — e.g. Symfony only emits
doctrine:migrations:migrate when doctrine-migrations-bundle is
installed; the MCP path honours that.
- Runs each command via podman exec -i -w <path> in the FPM container
matching the site's PHP version. No shell, no stdin, no prompts.
Same exec shape as execComposer / the CLI's execInContainer helper.
- A single failing step is reported in the returned text but the loop
keeps going — the default steps are idempotent by convention
(migrate, storage:link), so reporting and continuing is strictly
better than abort-on-first.
Description on the tool is deliberately specific: "Call after env_setup
on a fresh project." Weak local models lean on that phrasing to
sequence the bootstrap correctly. Idempotent is called out so agents
don't fret about re-running.
Interactive CLI lerd setup is unchanged. Humans still get the toggle
form; agents now have the same underlying capability without a prompt.
…on add-only
- ensureClaudeMCPRegistered drops the remove-then-add pattern. Only
calls `claude mcp add` when `claude mcp get lerd` reports missing.
Fixes the window where the remove succeeded and the add failed,
leaving the user with lerd unregistered in Claude Code.
- SKILL.md bootstrap and cloned-project workflows use the new `setup`
MCP tool instead of the per-framework `artisan migrate` /
`console doctrine:migrations:migrate` fork. Four-step sequence is
now project_new -> site_link -> env_setup -> setup for new
projects, and site_link -> composer install -> env_setup -> setup
for clones. Debug-500 flow calls setup() to run pending migrations.
- `env_setup` tool description now says "ALWAYS follow with setup to
run migrations". `setup` description says "MANDATORY after
env_setup on new or cloned projects". Weak local models lean on
these imperatives to sequence correctly.
- docs/features/mcp.md adds a `setup` row to the tool table, updates
the `env_setup` row, and rewrites the "create a new Laravel
project" and "set up the project I just cloned" example
interactions to the four-step sequence.
- docs/getting-started/{laravel,symfony,wordpress}.md mention the
new `setup` MCP tool in the AI-assistant tip at the top of each
getting-started page.
geodro
added a commit
that referenced
this pull request
Apr 22, 2026
…, install/uninstall polish (#241) - Bump internal/version to 1.18.0-beta.1. - CHANGELOG entry covering all 11 PRs since v1.17.1 (#229 through #240) in Keep-a-Changelog sections: Added / Changed / Fixed / Docs / CI. Breaking change is #232 (slim MCP tool manifest, merged action pairs). - docs/getting-started/installation.md: uninstall section now describes the four opt-in prompts (data, MCP integration, mkcert CA, images) and --force semantics. - docs/troubleshooting.md: new entry for the aardvark-dns drift case (every DNS lookup stalling ~5 seconds after a dual-stack migration). - docs/usage/lifecycle.md: new info block describing stale-site auto-cleanup — fsnotify fast path on parked dirs, 30s sweep across the full registry, eventbus publish so the dashboard refreshes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Consolidates four commits that together make the MCP-driven "scaffold and run a new site" flow work without the agent (or the user) shuffling intermediate steps. Observed problem: agents ran
project_new → site_link → env_setupand stopped there, leaving the site with emptyvendor/, no migrations, and a broken sqlite path.What it does
env_setupauto-creates the sqlite database when running non-interactively. Laravel's default.envhasDB_CONNECTION=sqlite; the CLI's interactive prompt to swap it for MySQL/Postgres was gated onisInteractive(), so MCP calls fell through without persisting the sqlite choice or creatingdatabase/database.sqlite. Non-interactive callers now default to sqlite, persist it to.lerd.yaml, and the existing file-creation block runs.project_newchasescomposer create-project --no-installwithcomposer installinside the FPM container. The scaffold command is deliberately--no-install --no-plugins --no-scriptsfor deterministic output; the tool now fills invendor/before returning so the reported success is actual success.New
setupMCP tool exposes the framework'sDefault: trueSetupentries (Laravel:storage:link+migrate; Symfony:doctrine:migrations:migrategated on thedoctrine-migrations-bundleCheck). Runs viapodman exec -i -w <path>in the site's FPM container, idempotent, non-fatal per step. No prompts; agents get the same underlying capability as the interactivelerd setupCLI without thehuhform.Claude Code registration self-heal.
IsMCPGloballyRegisteredswitched from the brokenclaude mcp list --scope user(rejected by newer Claude CLI) toclaude mcp get lerd.refreshGlobalMCPSkills(called from bothlerd installandlerd update) now calls a newensureClaudeMCPRegisteredhelper when markers say the user opted in globally but the Claude config doesn't know about lerd. The helper is add-only (skips the remove-then-add dance) so a transientaddfailure can't leave you unregistered.SKILL.md workflows replace the per-framework
artisan migrate/console doctrine:migrations:migratefork with a framework-agnostic four-step sequence:project_new → site_link → env_setup → setupsite_link → composer install → env_setup → setupsetup()to run pending migrations, thenstatus(), thendoctor().Tool descriptions on
env_setupandsetupare worded imperatively ("ALWAYS follow with setup", "MANDATORY after env_setup") so weaker local models (qwen, llama, mistral) sequence the bootstrap correctly.Docs
docs/features/mcp.mdadds asetuprow to the tool table, updates theenv_setuprow, and rewrites the "create a new Laravel project" and "set up the project I just cloned" example interactions to the four-step sequence.docs/getting-started/{laravel,symfony,wordpress}.mdmentionsetupin the AI-assistant tip.