Skip to content

feat(mcp): end-to-end agent-driven site bootstrap (project_new → setup)#240

Merged
geodro merged 4 commits intomainfrom
fix/agent-site-bootstrap
Apr 22, 2026
Merged

feat(mcp): end-to-end agent-driven site bootstrap (project_new → setup)#240
geodro merged 4 commits intomainfrom
fix/agent-site-bootstrap

Conversation

@geodro
Copy link
Copy Markdown
Owner

@geodro geodro commented Apr 22, 2026

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_setup and stopped there, leaving the site with empty vendor/, no migrations, and a broken sqlite path.

What it does

env_setup auto-creates the sqlite database when running non-interactively. Laravel's default .env has DB_CONNECTION=sqlite; the CLI's interactive prompt to swap it for MySQL/Postgres was gated on isInteractive(), so MCP calls fell through without persisting the sqlite choice or creating database/database.sqlite. Non-interactive callers now default to sqlite, persist it to .lerd.yaml, and the existing file-creation block runs.

project_new chases composer create-project --no-install with composer install inside the FPM container. The scaffold command is deliberately --no-install --no-plugins --no-scripts for deterministic output; the tool now fills in vendor/ before returning so the reported success is actual success.

New setup MCP tool exposes the framework's Default: true Setup entries (Laravel: storage:link + migrate; Symfony: doctrine:migrations:migrate gated on the doctrine-migrations-bundle Check). Runs via podman 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 interactive lerd setup CLI without the huh form.

Claude Code registration self-heal. IsMCPGloballyRegistered switched from the broken claude mcp list --scope user (rejected by newer Claude CLI) to claude mcp get lerd. refreshGlobalMCPSkills (called from both lerd install and lerd update) now calls a new ensureClaudeMCPRegistered helper 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 transient add failure can't leave you unregistered.

SKILL.md workflows replace the per-framework artisan migrate / console doctrine:migrations:migrate fork with a framework-agnostic four-step sequence:

  • New project: project_new → site_link → env_setup → setup
  • Cloned project: site_link → composer install → env_setup → setup
  • Debug a 500: ordered checklist ending in setup() to run pending migrations, then status(), then doctor().

Tool descriptions on env_setup and setup are 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.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 setup in the AI-assistant tip.

geodro added 4 commits April 22, 2026 22:13
…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 geodro merged commit 8781f11 into main Apr 22, 2026
3 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant