CLI

Kinetis ships one binary, bin/kinetis, installed as vendor/bin/kinetis once you composer require kinetis/kinetis. Running it with no arguments, or an unrecognized one, lists every available command:

php bin/kinetis
Usage: kinetis <command>

Available commands:
  mcp:serve — Starts the MCP server over stdio
  routes:list — Displays every discovered route and the full global middleware pipeline
  build — Compiles routes, MCP tools/resources, commands, and OpenAPI data ahead of time
  app:cleanup-sessions — Deletes sessions older than 30 days

Writing your own commands

Kinetis deliberately doesn’t schedule anything itself — that’s your infrastructure’s job (cron, a Kubernetes CronJob, an EventBridge rule, whatever you already use). What it gives you is a stable, named way to define a command so that infrastructure — or you, by hand — can actually run it:

namespace App\Console;

use Kinetis\Console\Attributes\Command;
use Kinetis\Console\CommandArguments;

final readonly class MaintenanceController
{
    #[Command('app:cleanup-sessions', description: 'Deletes sessions older than 30 days')]
    public function cleanupSessions(): void
    {
        // ...
    }

    #[Command('app:send-report', description: 'Emails a report to a given address')]
    public function sendReport(CommandArguments $arguments): int
    {
        $email = $arguments->get(0);

        if ($email === null) {
            fwrite(STDERR, "Usage: app:send-report <email>\n");
            return 1;
        }

        // ...
        return 0;
    }
}

Any class anywhere under one of your own PSR-4 roots is picked up automatically — App\Console\..., App\Domain\Orders\..., wherever you keep it. There’s no required directory or namespace convention to follow; organize commands however the rest of your application is organized. There’s nothing to register. Run a command by name:

vendor/bin/kinetis app:cleanup-sessions
vendor/bin/kinetis app:send-report ops@example.com --dry-run

A command method takes either no parameters, or exactly one parameter typed CommandArguments — everything else it needs (a database pool, a mailer, …) is constructor-injected, exactly like a controller. CommandArguments splits whatever followed the command’s own name into positional values (get(0), get(1), …) and --key=value/bare --flag options (option('key'), hasOption('flag')).

A command’s own return value becomes the process’s exit code — an int is used directly; void/null means success (0). This is the actual signal your scheduler reads to decide whether to alert or retry, so a command that can fail should say so with a non-zero return rather than only logging the problem. An uncaught exception is caught once, logged through whatever Psr\Log\LoggerInterface you’ve registered (see Logging), and also produces exit code 1.

MCP tools and resources (see Model Context Protocol (MCP)) and HTTP routes (see Routing & Validation) work the same way — discovered anywhere under your own PSR-4 roots, with no directory convention required.

kinetis build

php bin/kinetis build

Removes any existing .kinetis-cache/ and compiles a fresh one — routing, MCP, commands, and OpenAPI data. Run this as part of your deploy pipeline to pre-warm the cache before real traffic arrives — see Caching & AOT Compilation for exactly what gets written.

Always runs, regardless of APP_ENV — safe to run from a CI runner, a laptop, or any machine that hasn’t set that variable.

Pass --destroy to remove .kinetis-cache/ without rebuilding it:

vendor/bin/kinetis build --destroy

kinetis mcp:serve

php bin/kinetis mcp:serve

Starts Kinetis’s MCP server over stdio — one JSON-RPC message per line in, one per line out — the way Claude Desktop, Cursor, and most local MCP clients launch a server as a subprocess. Your own App\Mcp\... tools and resources are included automatically, alongside Kinetis’s own documentation resources — see Model Context Protocol (MCP) for the protocol itself and Caching & AOT Compilation for how production caching applies here.

kinetis routes:list

php bin/kinetis routes:list
Global middleware (outermost to innermost):
  1. Kinetis\Http\Middleware\ExceptionHandlerMiddleware
  2. App\Http\Middleware\RequestIdMiddleware

Method  Path     Status  Controller                       Middleware
------  -------  ------  -------------------------------  ---------------------------------------
GET     /orders  200     App\Http\OrderController::index  App\Http\Middleware\AuthMiddleware
POST    /orders  201     App\Http\OrderController::store  App\Http\Middleware\AuthMiddleware ->
                                                          App\Http\Middleware\RateLimitMiddleware

A read-only display tool — it never touches .kinetis-cache/ and never writes anything. Every invocation is a fresh, live discovery of your current source, regardless of APP_ENV, so it always reflects your code exactly as it stands right now, not whatever a stale compiled cache happens to hold.

The global middleware section lists the exact order requests run in — ExceptionHandlerMiddleware always first, then your own explicitly-registered (AppScope::middleware()) and #[AsGlobalMiddleware]-discovered classes, deduplicated (see Middleware). Each route’s own Middleware column shows its #[Middleware] list in the same class-level-then-method-level order it actually runs in, one middleware per line — every line but the last ends with -> to mark it continues on the next, so a route stacking several classes never forces one unreasonably wide line. A route with none shows .

Development vs. production

In development, commands are discovered fresh on every invocation, so a newly-added #[Command] method is picked up immediately. In production (APP_ENV=production, the default when unset), the binary loads its command list from the compiled cache kinetis build produces, compiling one automatically on the first invocation if none exists yet.

Restricting discovery

Once you’re relying on the compiled cache in production, scanning the whole application on every request is no longer the relevant cost — the scan only ever runs live in development, or once to build the cache. Even so, for a large enough codebase, that development-time scan can be worth bounding. COMMAND_DISCOVERY_PATHS (and its siblings MCP_DISCOVERY_PATHS/ROUTE_DISCOVERY_PATHS/MIDDLEWARE_DISCOVERY_PATHS/ LISTENER_DISCOVERY_PATHS for MCP, HTTP, global-middleware, and event- listener discovery — see Middleware/Events for the last two) restricts the scan to one or more comma-separated sub-paths, relative to each PSR-4 base directory your composer.json declares:

.env
COMMAND_DISCOVERY_PATHS=Console

With "App\\": "src/" in your autoload.psr-4, this restricts command discovery to src/Console/ — a class anywhere else under src/ is no longer scanned. Most small and medium applications never need this; it’s meant for a team that has measured a real, unacceptable scan cost and wants a deliberate, git-tracked restriction instead of relying on every developer to remember one. Kinetis’s own built-in commands, tools, resources, middleware, and listeners (under Kinetis\Console/Kinetis\Mcp/ Kinetis\Http/Kinetis\Events) are unaffected by any of these five variables either way — they’re always found in their own fixed location, never subject to your application’s own discovery scope.