Goal
Ship univeros/mcp — a first-party Model Context Protocol server that exposes the framework's capabilities as MCP tools, so any MCP-capable agent (Claude Desktop, Cursor, Zed, Codex, etc.) can drive an Altair project natively.
This is the framework's headline differentiator. Symfony doesn't ship this. Laravel doesn't ship this. Nobody in PHP ships this. With it, an agent can build, inspect, test, and ship an Altair API entirely through tool calls — without ever reading framework source code.
Why
The roadmap so far makes the framework agent-readable. The MCP server makes it agent-drivable. That's the difference between "an agent that can use this framework once you point it at the right files" and "an agent that prefers this framework because the tooling is native."
For context on MCP: https://modelcontextprotocol.io/ — it's the protocol Anthropic shipped for letting LLMs talk to local tooling. A server exposes "tools" (named, typed RPC endpoints); a client (Claude Desktop, Cursor, etc.) discovers them and surfaces them as callable actions in the agent's conversation.
Tools shipped in v1
Named with the framework__ prefix following MCP convention. All take JSON parameters, return JSON.
Discovery / inspection
| Tool |
Description |
Inputs |
framework__list_packages |
List every installed univeros/* package + a one-line description |
— |
framework__describe_package |
Full manifest content for one package (from univeros/agent-spec) |
package: string |
framework__list_endpoints |
All HTTP endpoints in the project (method, path, action class, spec path) |
— |
framework__describe_endpoint |
Spec + generated files + tests + OpenAPI fragment for one endpoint |
endpoint_id: string |
framework__list_commands |
All CLI commands registered (built-in + project) |
— |
framework__list_jobs |
All registered queue handlers + their message classes |
— |
framework__container_resolve |
What's bound to an interface in the container |
interface: string |
framework__list_specs |
All YAML specs under api/ |
— |
framework__read_spec |
Raw + parsed contents of one spec |
path: string |
Generation / mutation
| Tool |
Description |
Inputs |
framework__write_spec |
Create or update a YAML spec file (validates before writing) |
path: string, content: string |
framework__scaffold |
Run the scaffolder against a spec; return list of files emitted/modified |
spec_path: string, dry_run?: bool, force?: bool |
framework__rewind_spec |
Undo the last scaffold operation |
spec_path?: string |
framework__emit_openapi |
Return the full OpenAPI 3.1 document |
validate?: bool |
framework__emit_sdk |
Return generated SDK source for a language |
language: 'typescript' | 'python' |
Verification
| Tool |
Description |
Inputs |
framework__doctor |
Run health checks; return structured results |
only?: string[], skip?: string[] |
framework__run_tests |
Run PHPUnit; return structured pass/fail |
filter?: string, paths?: string[] |
framework__check_drift |
List spec-vs-code mismatches |
— |
framework__phpstan |
Run PHPStan with baseline; return errors |
level?: int |
Database (read-only by default, opt-in writes)
| Tool |
Description |
Inputs |
framework__db_query |
Run a SELECT against the dev database in a read-only sandbox |
sql: string |
framework__db_schema |
Dump current schema (tables, columns, indexes) |
— |
framework__db_migrate |
Apply pending migrations (write mode — disabled by default) |
dry_run?: bool |
Transport
MCP supports two transports: stdio and HTTP/SSE. Ship both.
bin/altair mcp serve # stdio (for Claude Desktop, Cursor)
bin/altair mcp serve --transport=http --port=3737 # HTTP (for remote agents)
The stdio transport is the common case (it's what Claude Desktop expects). HTTP is for advanced scenarios like agents running outside the project's container.
Wire format
Strict MCP 1.0 + JSON Schema for each tool's input/output. The server publishes its tool list on connect, the client discovers, the agent calls them. Schemas live in src/Altair/Mcp/Schema/*.json and are emitted at runtime.
Example tool registration (PHP-side):
#[McpTool(
name: 'framework__scaffold',
description: 'Run the spec scaffolder against a YAML spec, return list of emitted files',
inputSchema: __DIR__ . '/Schema/scaffold-input.json',
outputSchema: __DIR__ . '/Schema/scaffold-output.json',
)]
final class ScaffoldTool implements McpToolInterface
{
public function __construct(
private readonly Scaffolder $scaffolder,
) {}
public function call(array $input): array
{
$spec = $this->scaffolder->parse($input['spec_path']);
$result = $this->scaffolder->scaffold($spec, force: $input['force'] ?? false, dryRun: $input['dry_run'] ?? false);
return [
'emitted' => array_map(strval(...), $result->emitted),
'modified' => array_map(strval(...), $result->modified),
'skipped' => array_map(strval(...), $result->skipped),
];
}
}
Same Action/Input/__invoke contract the framework uses elsewhere — symmetric DX across HTTP, CLI, jobs, and now MCP tools.
Configuration in Claude Desktop / Cursor
A user adds the framework to their MCP client's config once:
{
"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 without further setup. "Add a POST /users endpoint that creates a user and sends a welcome email" becomes a sequence of framework__write_spec + framework__scaffold + framework__run_tests calls — no PHP source ever read.
Shape
src/Altair/Mcp/
├── Cli/
│ ├── ServeCommand.php # `bin/altair mcp serve`
│ └── ToolsListCommand.php # `bin/altair mcp tools` — list tools (debug)
├── Attribute/
│ └── McpTool.php
├── Contracts/
│ ├── McpToolInterface.php
│ └── TransportInterface.php
├── Transport/
│ ├── StdioTransport.php
│ └── HttpTransport.php # SSE-based
├── Protocol/
│ ├── Request.php
│ ├── Response.php
│ ├── ErrorResponse.php
│ └── Handshake.php # MCP 1.0 capability exchange
├── Schema/
│ └── *.json # one per tool input/output
├── Tool/ # built-in tool implementations
│ ├── ListPackagesTool.php
│ ├── DescribePackageTool.php
│ ├── ScaffoldTool.php
│ ├── RunTestsTool.php
│ ├── DoctorTool.php
│ └── ... # ~20 tools total in v1
├── ToolRegistry.php
├── Server.php # event loop
├── Configuration/
│ └── McpConfiguration.php
└── composer.json
Security boundaries
The MCP server runs locally as the developer's user — full filesystem access. We respect that and add guardrails for the dangerous bits:
- DB write tools (
db_migrate) require an explicit --allow-writes flag at mcp serve startup
- Writes to
vendor/, .git/, composer.json, .env* are blocked unconditionally
- Tools that modify files emit a structured "changeset" the client surfaces to the user before committing (the agent doesn't silently delete production code)
- A
--readonly flag turns the entire server into inspect-only
Acceptance criteria
Out of scope
- Hosting / multi-tenant (this is a local developer tool, one instance per project)
- Authentication on the HTTP transport (out-of-process scenarios assume same-trust-boundary; revisit if MCP gets a standard auth story)
- "Prompt" or "Resource" MCP primitives (tools are sufficient for v1; can add later)
- A web UI for the server (the client is the UI)
- Auto-installing into Claude Desktop's config (we document the snippet, users paste it)
Dependencies
A minimum-viable MCP server can ship after #17 + #18 with just the discovery/inspection tools. Mutation tools land alongside #19. This means the MCP server can demo the framework's "agent-native" position before the rest of the roadmap completes.
New composer dep:
- A pure-PHP JSON-Schema validator (
opis/json-schema: ^2.4 or similar)
No MCP SDK dependency — we implement the protocol directly. It's small (handshake + a few message types). Worth owning so we control the wire format and don't inherit JS-ecosystem habits.
Tagline once shipped
Drop the framework into your MCP client. Build APIs in chat. No source code reading required.
Goal
Ship
univeros/mcp— a first-party Model Context Protocol server that exposes the framework's capabilities as MCP tools, so any MCP-capable agent (Claude Desktop, Cursor, Zed, Codex, etc.) can drive an Altair project natively.This is the framework's headline differentiator. Symfony doesn't ship this. Laravel doesn't ship this. Nobody in PHP ships this. With it, an agent can build, inspect, test, and ship an Altair API entirely through tool calls — without ever reading framework source code.
Why
The roadmap so far makes the framework agent-readable. The MCP server makes it agent-drivable. That's the difference between "an agent that can use this framework once you point it at the right files" and "an agent that prefers this framework because the tooling is native."
For context on MCP: https://modelcontextprotocol.io/ — it's the protocol Anthropic shipped for letting LLMs talk to local tooling. A server exposes "tools" (named, typed RPC endpoints); a client (Claude Desktop, Cursor, etc.) discovers them and surfaces them as callable actions in the agent's conversation.
Tools shipped in v1
Named with the
framework__prefix following MCP convention. All take JSON parameters, return JSON.Discovery / inspection
framework__list_packagesuniveros/*package + a one-line descriptionframework__describe_packageuniveros/agent-spec)package: stringframework__list_endpointsframework__describe_endpointendpoint_id: stringframework__list_commandsframework__list_jobsframework__container_resolveinterface: stringframework__list_specsapi/framework__read_specpath: stringGeneration / mutation
framework__write_specpath: string, content: stringframework__scaffoldspec_path: string, dry_run?: bool, force?: boolframework__rewind_specspec_path?: stringframework__emit_openapivalidate?: boolframework__emit_sdklanguage: 'typescript' | 'python'Verification
framework__doctoronly?: string[], skip?: string[]framework__run_testsfilter?: string, paths?: string[]framework__check_driftframework__phpstanlevel?: intDatabase (read-only by default, opt-in writes)
framework__db_querysql: stringframework__db_schemaframework__db_migratedry_run?: boolTransport
MCP supports two transports: stdio and HTTP/SSE. Ship both.
The stdio transport is the common case (it's what Claude Desktop expects). HTTP is for advanced scenarios like agents running outside the project's container.
Wire format
Strict MCP 1.0 + JSON Schema for each tool's input/output. The server publishes its tool list on connect, the client discovers, the agent calls them. Schemas live in
src/Altair/Mcp/Schema/*.jsonand are emitted at runtime.Example tool registration (PHP-side):
#[McpTool( name: 'framework__scaffold', description: 'Run the spec scaffolder against a YAML spec, return list of emitted files', inputSchema: __DIR__ . '/Schema/scaffold-input.json', outputSchema: __DIR__ . '/Schema/scaffold-output.json', )] final class ScaffoldTool implements McpToolInterface { public function __construct( private readonly Scaffolder $scaffolder, ) {} public function call(array $input): array { $spec = $this->scaffolder->parse($input['spec_path']); $result = $this->scaffolder->scaffold($spec, force: $input['force'] ?? false, dryRun: $input['dry_run'] ?? false); return [ 'emitted' => array_map(strval(...), $result->emitted), 'modified' => array_map(strval(...), $result->modified), 'skipped' => array_map(strval(...), $result->skipped), ]; } }Same Action/Input/__invoke contract the framework uses elsewhere — symmetric DX across HTTP, CLI, jobs, and now MCP tools.
Configuration in Claude Desktop / Cursor
A user adds the framework to their MCP client's config once:
{ "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 without further setup. "Add a
POST /usersendpoint that creates a user and sends a welcome email" becomes a sequence offramework__write_spec+framework__scaffold+framework__run_testscalls — no PHP source ever read.Shape
Security boundaries
The MCP server runs locally as the developer's user — full filesystem access. We respect that and add guardrails for the dangerous bits:
db_migrate) require an explicit--allow-writesflag atmcp servestartupvendor/,.git/,composer.json,.env*are blocked unconditionally--readonlyflag turns the entire server into inspect-onlyAcceptance criteria
bin/altair mcp servestarts the stdio server and advertises ≥ 20 tools per the v1 listmcp-cli) — a smoke-test session creates a working endpoint end-to-endtools/listMCP methodContainer#[McpTool]attribute discoveryOut of scope
Dependencies
univeros/cli) —bin/altair mcp serveis a commanduniveros/agent-spec) —describe_packagereturns manifest contentuniveros/scaffold) —scaffold,write_spec,emit_openapi,emit_sdkwrap ituniveros/persistence) —db_*toolsbin/altair doctor) —framework__doctorwraps itA minimum-viable MCP server can ship after #17 + #18 with just the discovery/inspection tools. Mutation tools land alongside #19. This means the MCP server can demo the framework's "agent-native" position before the rest of the roadmap completes.
New composer dep:
opis/json-schema: ^2.4or similar)No MCP SDK dependency — we implement the protocol directly. It's small (handshake + a few message types). Worth owning so we control the wire format and don't inherit JS-ecosystem habits.
Tagline once shipped