Skip to content

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/altair directly: 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…UseWhy
A shell-capable agent (Claude Code, codex, etc.) + a local checkoutThe Altair skill + bin/altairLazy-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 shellMCPThe 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)MCPThis is what MCP was designed for — a standard wire format every client understands.
You’re introspecting a deployed Altair API over the networkMCP over HTTPmcp: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 in tests/Bootstrap/SkeletonGeneratorTest.php locks 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.

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).

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.

The package is bundled in the meta-package, so composer require univeros/framework already includes it. Standalone:

Terminal window
composer require univeros/mcp

It 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.

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:

Terminal window
bin/altair mcp:tools

That 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):

Terminal window
bin/altair mcp:tools --format=json

Start the server manually over stdio — one newline-delimited JSON-RPC message per line — to smoke-test a session by hand:

Terminal window
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:serve

You’ll get an initialize result (negotiated protocol version + server info) and a tools/call result whose structuredContent lists every installed package.

The server has a clean split between protocol, transport, and tools.

  • ProtocolServer\Server is 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 handles initialize (with protocol-version negotiation in Server\ServerInfo), ping, tools/list, tools/call, and the notifications/* 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 successful tools/call result with isError: true — the MCP convention that a tool failure is data the model reacts to, not a transport error.
  • Transport — implementations of Contracts\TransportInterface carry the framed messages. Transport\StdioTransport is newline-delimited JSON over stdin/stdout (desktop clients); Transport\HttpTransport answers one JSON-RPC message per POST (out-of-process agents); Transport\InMemoryTransport queues messages for tests. Server\ServerRunner pumps 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 the Attribute\McpTool attribute for its name, description, and input/output JSON schema paths. Tool\AttributeToolDiscoverer reads the attribute into a Tool\ToolDescriptor; Tool\ToolRegistry holds the set (name-sorted for stable tools/list output); Tool\ContainerToolResolver instantiates a tool through the Container at call time, so a tool’s constructor dependencies are autowired exactly like a CLI command’s.
  • SchemasSchema\SchemaValidator validates a tool’s arguments against its input schema (via opis/json-schema) before the tool runs. Built-in schemas live in src/Altair/Mcp/Schema/*.json and are published verbatim through tools/list.

The guardrails are what make it safe to point an agent at your filesystem:

  • Guard\PathGuard blocks writes to vendor/, .git/, composer.json, composer.lock, and any .env* file, and confines reads/spec-loads to the project root (lexical, traversal-safe).
  • Guard\ServerMode is the mutation policy set at startup: --readonly makes the whole server inspect-only; database writes additionally require --allow-writes.
  • Database\SqlReadGuard restricts framework__db_query to a single read-only SELECT/WITH and rejects writes, DDL, statement chaining, and INTO OUTFILE/DUMPFILE.

All 29 tools use the framework__ prefix, take JSON arguments, and return JSON. They fall into five groups:

Discovery / inspectionlist_packages, describe_package, list_specs, read_spec, list_endpoints, describe_endpoint, container_resolve, list_commands.

Generation / mutationwrite_spec (validates before writing), scaffold, rewind_spec, emit_openapi, emit_sdk.

Verificationdoctor, run_tests, check_drift, phpstan.

Database (read-only by default) — db_query, db_schema, db_migrate, plan_migration.

Introspectioncontainer_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.

The stdio transport is the common case and the default:

Terminal window
bin/altair mcp:serve

For an out-of-process agent, switch to HTTP (one JSON-RPC message per POST):

Terminal window
bin/altair mcp:serve --transport=http --host=127.0.0.1 --port=3737

Lock the server down when you only want inspection, or opt into gated database writes:

Terminal window
bin/altair mcp:serve --readonly # no writes at all
bin/altair mcp:serve --allow-writes # permit db:migrate via framework__db_migrate

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\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.

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 through InMemoryTransport against a Container-built Server: initialize, tools/list, then real tools/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.

  • cli.mdmcp:serve and mcp:tools are attribute-driven commands discovered by the framework CLI.
  • scaffold.md — the framework__write_spec / scaffold / rewind_spec / emit_openapi / emit_sdk tools wrap it.
  • introspection.md — the framework__container_inspect / routes_* / listeners_* / middleware_list / config_dump / manifest_diff tools wrap its inspectors.
  • doctor.mdframework__doctor wraps the health-check runner.
  • events.md — mutating tools record what they changed to the .altair/events.jsonl log.
  • agent-spec.mdframework__describe_package surfaces package shape; AgentSpec is the agent-readable counterpart to this agent-drivable server.
  • persistence.md — backs the framework__db_* tools when wired.
  • 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.1 and assumes a same-trust-boundary caller).
  • The database tools require univeros/persistence to be wired; without it db_query / db_schema report that no database is configured, and db_migrate is gated behind --allow-writes.
  • config_dump masks secrets by key name (e.g. anything containing PASSWORD, 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.