Mcp
A first-party Model Context Protocol server that exposes the framework as agent-callable tools — the shell-less bridge for clients that can’t drive
bin/altairdirectly: hosted agents, non-Claude MCP clients, or anything introspecting a deployed Altair API over HTTP.
Composer: univeros/mcp
Namespace: Altair\Mcp
When to use MCP vs. the skill + bin/altair
Section titled “When to use MCP vs. the skill + bin/altair”The framework ships two agent interfaces, and they’re optimised for different clients. Pick the one that matches yours:
| You have… | Use | Why |
|---|---|---|
| A shell-capable agent (Claude Code, codex, etc.) + a local checkout | The Altair skill + bin/altair | Lazy-loaded — the skill’s name and description sit in the agent’s “available skills” list at near-zero cost, and the body only loads when invoked. Zero schema dump per turn. |
| A remote / hosted agent, no shell | MCP | The agent doesn’t have a subprocess to call. MCP gives it a structured way to drive the framework over JSON-RPC. |
| A non-Claude MCP client (Cursor, Zed, an in-house client speaking MCP) | MCP | This is what MCP was designed for — a standard wire format every client understands. |
| You’re introspecting a deployed Altair API over the network | MCP over HTTP | mcp:serve --transport=http answers one JSON-RPC message per POST; no SSH or local checkout required. |
If you’re a shell-capable agent reading this and you’ve already opened the package source, you’ve taken the expensive path. The cheap path is the Altair skill — pre-installed in every project generated by bin/altair new at two paths so both Claude Code and any agent implementing the cross-agent Agent Skills spec (Junie, etc.) find it out of the box:
.ai/skills/altair/SKILL.md— canonical, cross-agent (Anthropic Agent Skills format; the convention Laravel Boost ships its skills under)..claude/skills/altair/SKILL.md— Claude Code’s project-skills path. Byte-equal copy of the canonical file (a PHPUnit test intests/Bootstrap/SkeletonGeneratorTest.phplocks them together; if you edit one, edit both).
The skill teaches the agent to drive the framework over bash and to read .agent/ manifests for the API shape, without injecting every MCP tool’s JSON schema into the context window on each turn.
This isn’t a hierarchy of quality — MCP is a first-class feature and it earns its place above. It just isn’t the default interface for the most common case, and the docs used to imply it was.
Introduction
Section titled “Introduction”The rest of the framework makes your project agent-readable — typed code, deterministic manifests, JSON-emitting CLI commands. This package makes it agent-drivable for clients that can’t reach for the shell. The Model Context Protocol is the open standard MCP clients (Claude Desktop, Cursor, Zed, …) speak to local tooling: a server advertises named, typed “tools”; the client discovers them; the agent calls them as first-class actions in its conversation. You point your MCP client at bin/altair mcp:serve once, and from then on “add a POST /users endpoint that creates a user” becomes a sequence of framework__write_spec + framework__scaffold + framework__run_tests calls — no file reading, no shelling out.
You’ll reach for this package when MCP is your only channel — your client can’t subprocess bin/altair itself. It ships 29 built-in tools out of the box and lets you register your own with a single attribute.
The server is implemented directly on JSON-RPC 2.0 — there is no third-party MCP SDK in the dependency tree. The protocol is small (a handshake plus a few message types), and owning it keeps the wire format under your control. The protocol “brain” is decoupled from the bytes on the wire, so the same server runs over stdio (what desktop clients expect), over HTTP (for out-of-process agents), or over an in-memory transport (for tests).
A note on context cost
Section titled “A note on context cost”tools/list is on-demand by protocol, but most MCP clients eagerly inject every advertised tool into the model’s system prompt — so the 29 tools’ schemas take context every turn whether the agent calls them or not. If your client supports a curated tool palette or deferred-load tool discovery, prefer that. We track the server-side mitigations (curated default set behind a flag, a tools/search meta-tool mirroring the deferred-tool pattern) in #131; the current decision is that the doc reframe above plus the always-on skill is enough to redirect the eager-load path away from MCP for shell-capable agents, and that a tool-trim is a follow-up only worth doing once a concrete client demands it.
Installation
Section titled “Installation”The package is bundled in the meta-package, so composer require univeros/framework already includes it. Standalone:
composer require univeros/mcpIt depends on univeros/cli (the mcp:serve / mcp:tools commands), univeros/container (tools autowire their dependencies), and the packages whose capabilities the built-in tools wrap (univeros/scaffold, univeros/introspection, univeros/doctor, univeros/events, univeros/agent-spec). It also pulls in opis/json-schema — a pure-PHP JSON Schema validator used to validate every tool call. The database tools additionally use univeros/persistence when it is wired; without it they report that no database is configured rather than failing.
Quick start
Section titled “Quick start”Add the server to your MCP client’s config once. For Claude Desktop, that’s claude_desktop_config.json:
{ "mcpServers": { "altair": { "command": "php", "args": ["/path/to/project/bin/altair", "mcp", "serve"], "env": { "APP_ENV": "dev" } } }}From that point on the agent has the full tool palette. To see what it can call without wiring up a client, list the tools yourself:
bin/altair mcp:toolsThat prints all 29 tools and their descriptions. For the machine-readable form (name + input/output JSON schema per tool, exactly what tools/list returns over the wire):
bin/altair mcp:tools --format=jsonStart the server manually over stdio — one newline-delimited JSON-RPC message per line — to smoke-test a session by hand:
printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"framework__list_packages","arguments":{}}}' \ | bin/altair mcp:serveYou’ll get an initialize result (negotiated protocol version + server info) and a tools/call result whose structuredContent lists every installed package.
Concepts
Section titled “Concepts”The server has a clean split between protocol, transport, and tools.
- Protocol —
Server\Serveris the pure message brain: it turns one inbound JSON-RPC message into one outbound message (or null for a notification), with no knowledge of the transport. It handlesinitialize(with protocol-version negotiation inServer\ServerInfo),ping,tools/list,tools/call, and thenotifications/*messages. Protocol-level problems (malformed JSON, unknown method, bad params, batch requests) come back as JSON-RPC errors; a tool that throws comes back as a successfultools/callresult withisError: true— the MCP convention that a tool failure is data the model reacts to, not a transport error. - Transport — implementations of
Contracts\TransportInterfacecarry the framed messages.Transport\StdioTransportis newline-delimited JSON over stdin/stdout (desktop clients);Transport\HttpTransportanswers one JSON-RPC message per POST (out-of-process agents);Transport\InMemoryTransportqueues messages for tests.Server\ServerRunnerpumps a streaming transport: read a message, dispatch it, write the reply, repeat. - Tools — every tool is a class implementing
Contracts\McpToolInterface(call(array $input): array) and carrying theAttribute\McpToolattribute for its name, description, and input/output JSON schema paths.Tool\AttributeToolDiscovererreads the attribute into aTool\ToolDescriptor;Tool\ToolRegistryholds the set (name-sorted for stabletools/listoutput);Tool\ContainerToolResolverinstantiates a tool through the Container at call time, so a tool’s constructor dependencies are autowired exactly like a CLI command’s. - Schemas —
Schema\SchemaValidatorvalidates a tool’sargumentsagainst its input schema (via opis/json-schema) before the tool runs. Built-in schemas live insrc/Altair/Mcp/Schema/*.jsonand are published verbatim throughtools/list.
The guardrails are what make it safe to point an agent at your filesystem:
Guard\PathGuardblocks writes tovendor/,.git/,composer.json,composer.lock, and any.env*file, and confines reads/spec-loads to the project root (lexical, traversal-safe).Guard\ServerModeis the mutation policy set at startup:--readonlymakes the whole server inspect-only; database writes additionally require--allow-writes.Database\SqlReadGuardrestrictsframework__db_queryto a single read-onlySELECT/WITHand rejects writes, DDL, statement chaining, andINTO OUTFILE/DUMPFILE.
The built-in tools
Section titled “The built-in tools”All 29 tools use the framework__ prefix, take JSON arguments, and return JSON. They fall into five groups:
Discovery / inspection — list_packages, describe_package, list_specs, read_spec, list_endpoints, describe_endpoint, container_resolve, list_commands.
Generation / mutation — write_spec (validates before writing), scaffold, rewind_spec, emit_openapi, emit_sdk.
Verification — doctor, run_tests, check_drift, phpstan.
Database (read-only by default) — db_query, db_schema, db_migrate, plan_migration.
Introspection — container_inspect, config_dump (secrets always masked), routes_list, route_show, listeners_list, listener_show, middleware_list, manifest_diff.
Each one wraps the real framework API — framework__scaffold drives univeros/scaffold, framework__doctor drives univeros/doctor, the framework__*_inspect/*_list tools drive univeros/introspection — so the tool surface and the CLI surface stay in lock-step.
Running the server
Section titled “Running the server”The stdio transport is the common case and the default:
bin/altair mcp:serveFor an out-of-process agent, switch to HTTP (one JSON-RPC message per POST):
bin/altair mcp:serve --transport=http --host=127.0.0.1 --port=3737Lock the server down when you only want inspection, or opt into gated database writes:
bin/altair mcp:serve --readonly # no writes at allbin/altair mcp:serve --allow-writes # permit db:migrate via framework__db_migrateWriting a custom tool
Section titled “Writing a custom tool”A tool is a plain service: implement McpToolInterface and decorate it with #[McpTool]. The input has already been validated against your schema before call() runs, and the Container autowires your constructor — so you can depend on any bound service.
use Altair\Mcp\Attribute\McpTool;use Altair\Mcp\Contracts\McpToolInterface;use Override;
#[McpTool( name: 'app__greet', description: 'Return a greeting for the given name.', inputSchema: __DIR__ . '/Schema/greet-input.json',)]final readonly class GreetTool implements McpToolInterface{ #[Override] public function call(array $input): array { return ['greeting' => 'Hello, ' . ($input['name'] ?? 'world')]; }}inputSchema is an absolute path to a JSON Schema file (the attribute is evaluated at compile time, so use __DIR__ . '/…' rather than a function call). Point the server at the directory holding your tools with the MCP_TOOL_PATHS environment variable (PATH_SEPARATOR-delimited) and AttributeToolDiscoverer finds them at startup. Throw Altair\Mcp\Exception\GuardrailException from a tool when an operation should be refused — the server surfaces its message to the agent as a tool error rather than a crash.
Configuration
Section titled “Configuration”Configuration\McpConfiguration wires everything into the Container: the tool registry (built-in tools from Tool\BuiltinTools plus any discovered user tools), the protocol services, the guardrails, and the read-only database gateway (Database\CycleDatabaseGateway when univeros/persistence is bound, otherwise Database\NullDatabaseGateway). It applies the prerequisite Configurations (events, scaffold journal, doctor) when they are absent, so tool dependencies resolve, and binds the Container to itself so tools resolve through the real instance.
You rarely construct it by hand — mcp:serve and mcp:tools apply it for you, passing the ServerMode derived from the command flags. Construct it directly only when embedding the server in another process:
use Altair\Container\Container;use Altair\Mcp\Configuration\McpConfiguration;use Altair\Mcp\Guard\ServerMode;use Altair\Mcp\Server\Server;
$container = new Container();(new McpConfiguration(mode: new ServerMode(readonly: true)))->apply($container);
/** @var Server $server */$server = $container->make(Server::class);$reply = $server->handle('{"jsonrpc":"2.0","id":1,"method":"tools/list"}');See container.md for the binding API.
Testing
Section titled “Testing”The tests under tests/Mcp/ are the clearest description of the server’s behaviour:
tests/Mcp/Server/ServerTest.php— the protocol surface: handshake, ping, tool listing, tool invocation, and every error path (parse error, unknown method, bad params, guardrail-as-tool-error).tests/Mcp/Tool/*ToolsTest.php— each tool group, with golden input/output cases and the graceful-degradation paths (e.g. an introspection tool returning{available: false}when its host collaborator isn’t bound).tests/Mcp/McpServerIntegrationTest.php— a full agent-style session driven throughInMemoryTransportagainst a Container-builtServer:initialize,tools/list, then realtools/calls.
When you add a tool, mirror that last pattern: build the registry, drive a session over the in-memory transport, and assert the structured result — it exercises validation, resolution, and the protocol envelope together.
Related packages
Section titled “Related packages”- cli.md —
mcp:serveandmcp:toolsare attribute-driven commands discovered by the framework CLI. - scaffold.md — the
framework__write_spec/scaffold/rewind_spec/emit_openapi/emit_sdktools wrap it. - introspection.md — the
framework__container_inspect/routes_*/listeners_*/middleware_list/config_dump/manifest_difftools wrap its inspectors. - doctor.md —
framework__doctorwraps the health-check runner. - events.md — mutating tools record what they changed to the
.altair/events.jsonllog. - agent-spec.md —
framework__describe_packagesurfaces package shape; AgentSpec is the agent-readable counterpart to this agent-drivable server. - persistence.md — backs the
framework__db_*tools when wired.
Limitations
Section titled “Limitations”- This is a local developer tool that runs as your user with your filesystem permissions — that’s the accepted trust model. The guardrails defend against an agent doing more than intended; they are not a sandbox against a hostile operator.
- One instance per project. Hosting, multi-tenancy, and authentication on the HTTP transport are out of scope for v1 (the HTTP transport binds to
127.0.0.1and assumes a same-trust-boundary caller). - The database tools require
univeros/persistenceto be wired; without itdb_query/db_schemareport that no database is configured, anddb_migrateis gated behind--allow-writes. config_dumpmasks secrets by key name (e.g. anything containingPASSWORD,TOKEN,DSN, …). A secret stored under a non-matching key — or inline in a URL value — can still surface; treat the output as sensitive.- The server implements MCP tools only; the protocol’s “resources” and “prompts” primitives are not exposed in v1.