The composer create-project starting point for a Milpa app: boots milpa/runtime's Kernel, serves a real HTTP response with zero database, and ships a coa CLI (doctor/validate/make/inspect, plus every plugin-declared CommandProviderInterface command) wired to milpa/devtools. Minimal by default — the agent-ready surface (MCP/tools, bin/mcp-server.php) is opt-in via `composer require milpa/tool-runtime milpa/mcp-server` or `php bin/coa agent:enable`.
composer create-project milpa/skeleton myapp→ an app that runs — booted, serving/, answeringcoa— with zero database. This is your starting point, not a demo.
milpa/skeleton is the smallest real host of milpa/runtime: a Kernel::boot() call, one
plugin, one route, one CLI. No Doctrine, no legacy Milpa\Web, nothing to configure before you
see it work.
composer create-project milpa/skeleton myapp
cd myapp
php -S localhost:8000 -t public
Open http://localhost:8000 — you'll see "Milpa is running", served by
App\Plugins\HelloPlugin\Controllers\HomeController through Milpa\Runtime\Http\RequestHandler.
The page points at the same first-five-minutes loop the CLI can print for you:
php bin/coa wowThe first-five-minutes path
Milpa's skeleton is intentionally small, but it is not a blind hello world. The "wow" is the closed evidence loop: create → inspect → extend → validate → expose to agents.
Start by asking the app what actually booted:
php bin/coa doctor php bin/coa inspect:routes php bin/coa inspect:commands
doctor should report the stock app's single plugin, single route, config value, and zero database
queries:
milpa · coa doctor
root: /path/to/myapp
✔ 1 plugin(s) configured, 1 booted: HelloPlugin
✔ container: Milpa\Container\DIContainer
✔ dispatcher: Milpa\Eventing\EventDispatcher
✔ 1 route(s) declared (RouteProviderInterface plugins)
✔ config: app.greeting = 'Milpa is running.'
✔ kernel booted — zero database queries.
Now make the smallest visible change and inspect it before trusting it:
php bin/coa make:controller DemoPlugin DemoController --path=/demo --register php bin/coa inspect:routes php bin/coa validate
Serve the app and hit the new route:
php -S localhost:8000 -t public curl http://localhost:8000/demo
When you want the agent surface, opt in explicitly:
php bin/coa agent:enable php bin/coa inspect:tools printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \ | php bin/mcp-server.php
A stock app has no tools yet; that is honest. The point is that the app can now expose whatever
#[Tool] methods its booted plugins register, and both humans and agents can list that surface
before calling it.
| Path | What it is |
|---|---|
public/index.php |
The HTTP entry point: builds a PSR-7 request from globals, boots the kernel, dispatches through Milpa\Runtime\Http\RequestHandler, emits the response. |
bin/coa |
The CLI entry point — doctor, validate, make:controller/entity/plugin/service/tool/crud, inspect:plugins/routes/services/tools/commands, agent:enable. See src/Console/Application.php. |
bin/mcp-server.php |
The generic MCP stdio server. Only serves once agent-ready is turned on (see below) — a stock app prints guidance and exits 0. |
config/plugins.php |
What this app has — a plain list<class-string> you edit and read in a diff. No database, no filesystem discovery. |
config/boot.php |
What every entry point needs before the kernel runs: the container, and the plugins that actually boot. It resolves config/plugins.php against the activation store in one call, so the store the app boots from and the store the plugins.* operations write to cannot become two different files. |
storage/plugins.json |
What is switched on. Written only when somebody enables, disables or installs something — an app that manages nothing never grows it. Deployment state, not template content. |
plugins/ |
Where the installer puts plugins fetched at runtime, under the Milpa\Plugins namespace. Plugins this app owns live in src/Plugins/. |
config/app.php |
The app-config bag. Registered by Kernel::boot() as Milpa\Runtime\Config; plugins read it in boot(). See The Config idiom. |
src/Plugins/HelloPlugin/ |
The example plugin: #[PluginMetadata], no provides/requires, one GET / route, and the Config read that drives the homepage greeting. Copy its shape for your own plugins. |
tests/Boot/KernelBootTest.php |
The boot smoke test: the kernel boots from config/boot.php and GET / returns 200. |
tests/Boot/PluginActivationTest.php |
Switching a plugin off actually stops it — end to end, through the same config/boot.php every entry point loads. |
PluginManagementPlugin is the first line in config/plugins.php, and it is what makes plugins
manageable at all: it contributes the plugins.* operations, which the terminal, HTTP and MCP each
project into their own shape. One definition, every surface.
php bin/coa plugins.list # what this app has, and what boots php bin/coa plugins.disable --name=HelloPlugin php bin/coa inspect:routes # its route is gone php bin/coa plugins.enable --name=HelloPlugin
What this app has is config/plugins.php — code. What is switched on is
storage/plugins.json — state. A plugin in the list boots unless the store says otherwise, so an
app that never manages anything behaves exactly as if the list were the only thing there is, and
never grows the file. Keeping them apart is what lets an admin surface switch plugins without
writing PHP back to disk: a page that rewrites its own source is a code-execution write surface,
and it breaks the moment a deploy is read-only.
Installing is a separate capability. Wire a Milpa\Interfaces\Plugin\PluginInstallerInterface
into config/boot.php's container and plugins.install / plugins.update / plugins.remove
appear too — until then they simply do not exist, so no surface renders a button that fails when
pressed. Those three declare requiresConfirmation and a stricter scope than the toggles, because
they fetch and run code that was not here a moment ago. A freshly installed plugin arrives
disabled: installing is not consenting to run it.
Delete the PluginManagementPlugin line and this app goes back to being governed by the list alone.
The stock app is minimal: composer create-project milpa/skeleton pulls no AI/MCP packages, and
require in composer.json lists none. bin/mcp-server.php and coa inspect:tools both still run
— they just report that the surface isn't on yet, cleanly, with no fatal.
Turn it on when you actually want to expose this app's tools over MCP:
php bin/coa agent:enable
# — a thin wrapper over —
composer require milpa/tool-runtime milpa/mcp-server
Either one installs milpa/tool-runtime (the #[Tool] attribute + registry) and milpa/mcp-server
(the JSON-RPC/stdio transport). Once installed, bin/mcp-server.php serves every #[Tool] a booted
ToolProviderInterface plugin registers, and coa inspect:tools lists them. coa make:tool prints
the same "not detected, run composer require milpa/tool-runtime" guidance if you scaffold a tool
before opting in.
src/Plugins/YourPlugin/YourPlugin.php implementing Milpa\Interfaces\Plugin\PluginInterface
with a #[Milpa\Attributes\PluginMetadata(...)] attribute (copy HelloPlugin's shape).Milpa\Runtime\Http\RouteProviderInterface::routes() —
return a list<Milpa\Http\Routing\Route>, each bound to a
Milpa\Http\Routing\HandlerReference(ControllerClass::class, 'method').Psr\Http\Message\ServerRequestInterface and returns
a Psr\Http\Message\ResponseInterface (see HomeController — this skeleton ships
nyholm/psr7 as its PSR-7 implementation, since milpa/http deliberately ships contracts
only, no concrete request/response classes).config/plugins.php.php bin/coa doctor to confirm it booted and its routes were counted; php bin/coa validate
for a static pre-boot capability check without running boot().Milpa\Runtime\Kernel::boot() capability-checks every configured plugin's #[PluginMetadata]
before anything boots — a requires with no matching provides fails loudly, pre-boot, with
a typed PluginDependencyException, not a runtime surprise three requests later.
A plugin's constructor is fixed by Milpa\Interfaces\Plugin\PluginInterface to a single argument
— (DIContainerInterface $container). It never receives config values directly. So how does a
plugin get a storage path, an API base URL, or a greeting string? It reads them in boot()
from the app-config bag:
Put configuration in config/app.php, which returns a nested array. Dot-notation indexes it,
so ['app' => ['greeting' => 'Hi']] is read back as app.greeting.
public/index.php (and bin/coa) pass it into the kernel:
$boot = require $root . '/config/boot.php'; $kernel = Kernel::boot([ 'plugins' => $boot['plugins'], 'config' => require $root . '/config/app.php', 'container' => $boot['container'], ]);
Kernel::boot() registers it in the container as Milpa\Runtime\Config. A plugin reads what
it needs in boot() — this is exactly what HelloPlugin does:
public function boot(): void { $greeting = $this->container->get(Config::class)->get('app.greeting', 'Milpa is running.'); $this->container->registerService(HomeController::class, new HomeController($greeting)); }
Edit app.greeting in config/app.php, reload http://localhost:8000, and the heading changes
— no env vars, no constructor plumbing. That is the whole idiom: config lives in a file,
plugins read it in boot(), coa doctor echoes back the value it resolved.
coabin/coa wires milpa/devtools' generate/inspect layer straight into this project — including
for an agent driving the CLI, not just a human:
php bin/coa doctor # boot the kernel, report what came up php bin/coa validate # static capability check, no boot() php bin/coa make:controller PingPlugin PingController --path=/ping --register php bin/coa make:entity BoardPlugin Task --fields="title:string:200,done:bool" --wire php bin/coa make:plugin BoardPlugin --provides=board.capability php bin/coa make:service BoardPlugin WorkflowService --interface php bin/coa make:tool BoardPlugin CompleteTaskTool --description="Mark a task done" php bin/coa make:crud BoardPlugin Task --fields="title:string:200,status:string:20" --register php bin/coa inspect:plugins # booted plugins + their capability graph php bin/coa inspect:routes # the booted route table php bin/coa inspect:services # what the DI container has registered php bin/coa inspect:tools # registered #[Tool]s (or "agent-ready not enabled") php bin/coa agent:enable # opt in: composer require tool-runtime + mcp-server
make:controller is real milpa/devtools machinery (Milpa\DevTools\Make\Generators\ControllerGenerator
WriteGuard), and it scaffolds code that boots in this skeleton unchanged. devtools
auto-detects the convention per app root (Milpa\DevTools\Make\ConventionDetector): this project
has config/plugins.php and an App\ PSR-4 root with no milpa.json, so it picks the runtime
flavor with no flag and writes exactly this skeleton's App\Plugins\* + RouteProviderInterface
pattern — a plain PSR-7 controller (index(ServerRequestInterface): ResponseInterface, no base
class, no #[Route]) plus a minimal RouteProviderInterface plugin wiring GET <path> → Controller::index. By default the command prints the registration step so you can review the app
boundary yourself; pass --register when you want coa to add the generated plugin class to
config/plugins.php for you. Do that, reload, and the new route serves a real response. Pass
--flavor=legacy to force the old Milpa\Plugins\* + BaseController host convention instead;
--path=/route sets the route path.The same Generator + WriteGuard engine backs make:entity (a persisting domain model +
FileRepository), make:plugin (a standalone composition unit), make:service (a DI-registered
domain service, optionally with a companion interface via --interface), make:tool (a
#[Tool]-attributed AI-callable method — requires composer require milpa/tool-runtime in your
own project to actually load), and make:crud (entity + a 5-method REST controller + routes +
wiring plugin, composed from make:entity plus a new controller shape). make:entity --wire is an
explicit convenience splice for an existing plugin that carries the // {coa:services} marker: it
inserts the repository registration for you, but it is intentionally honest that this is not a
design review — check whether that plugin is really the right composition boundary. Run php bin/coa
with no arguments for the full command/option reference. The inspect:* commands boot the real kernel and
report what they find — inspect:services reaches into the concrete
Symfony\Component\DependencyInjection\ContainerBuilder under Milpa\Container\DIContainer (the
DI contract itself exposes no enumeration method), and inspect:routes reconstructs the route
table from every booted RouteProviderInterface plugin (Milpa\Http\Routing\Router exposes no
route-table accessor of its own).
milpa/runtime's plugin registry is config-driven, never a
Doctrine entity — persistence is something a plugin opts into, never something the kernel
requires. Add doctrine/orm and a storage plugin when (if) you need one.nyholm/psr7 is declared here as a real dependency because
milpa/http ships routing contracts only, no concrete request/response implementation. Swap
it for another PSR-7/PSR-17 implementation if you prefer — public/index.php and
RequestHandler only depend on the PSR interfaces.This skeleton composes eight published Milpa packages, unmodified:
milpa/runtime — the bootable kernel that
wires the rest togethermilpa/resolver — resolves the architecture
before booting it: the report validates the graph, orders the boot, and turns failures into
learnable errorsmilpa/core — contracts, capability graph, eventsmilpa/container — the DI containermilpa/events — the event dispatchermilpa/http — PSR-15-native routing contractsmilpa/plugin — the plugin contractsmilpa/devtools — the engine behind bin/coaContributions are welcome. Please report security issues responsibly, and note that this project follows a standard code of conduct.
LicenseApache-2.0 © Rodrigo Vicente - TeamX Agency.
Milpa is designed, built, and maintained by Rodrigo Vicente - TeamX Agency.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | milpa/admin (v0.1.0) | 0 | 11.16 | 28-07-2026 |
| 2 | milpa/auth (v0.3.0) | 0 | 10.94 | 28-07-2026 |
| 3 | milpa/http-symfony (v0.1.0) | 0 | 12.31 | 28-07-2026 |
| 4 | milpa/live (v0.2.0) | 0 | 17.79 | 28-07-2026 |
| 5 | milpa/orchestrator (v0.3.0) | 0 | 16.04 | 28-07-2026 |
| 6 | milpa/live-tui (v0.3.0) | 0 | 12.4 | 28-07-2026 |
| 7 | milpa/live-web (v0.2.0) | 0 | 18.48 | 28-07-2026 |
| 8 | seip25/lila-php (v1.59) | 0 | 38 | 28-07-2026 |
| 9 | shappetrack 1.0.6 | 0 | 5 | 10-07-2026 |
| 10 | elphos 0.3.0 | 0 | 7 | 10-07-2026 |