Skip to content

univeros/mcp — Model Context Protocol server for agent-native workflows #69

@tonydspaniard

Description

@tonydspaniard

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

  • bin/altair mcp serve starts the stdio server and advertises ≥ 20 tools per the v1 list
  • Tested against a real MCP client (Claude Desktop or mcp-cli) — a smoke-test session creates a working endpoint end-to-end
  • All built-in tools have JSON Schema for input and output; both are emitted via tools/list MCP method
  • Tools resolve their dependencies from the framework's Container
  • User-defined tools register via #[McpTool] attribute discovery
  • Guardrails enforced (vendor write blocked, db writes gated, readonly mode works)
  • HTTP transport works for at least one out-of-process scenario
  • Tests:
    • Each built-in tool unit-tested (input → output golden cases)
    • Protocol layer: handshake, tool listing, tool invocation, error responses
    • Guardrails: blocked writes return proper MCP errors, not exceptions
    • Integration: full session via in-memory transport, agent-style script

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions