Search by

quioteframework / quiote-mcp-assistant

MCP server giving AI agents authoritative knowledge of, and read-only/scaffolding access to, Quiote applications.

Maintainers

Package info

github.com/quioteframework/quiote-mcp-assistant

Type:project

pkg:composer/quioteframework/quiote-mcp-assistant

Transparency log

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.3.0 2026-08-11 13:42 UTC

This package is auto-updated.

Last update: 2026-09-01 12:08:14 UTC


README

CI codecov Latest release PHP PHPStan level 9 License: MIT

An MCP server that gives an AI agent (Claude Code, Cursor, GitHub Copilot, …) authoritative knowledge of the Quiote PHP framework, plus tools to introspect and scaffold code in a real Quiote app. It's built as a Quiote app itself, dogfooding the framework's own app-as-MCP-server capability.

Point it at nothing and you get a documentation/reference assistant. Point it at a project with --target-app-dir and you also get tools that inspect that project's routes, config, plugins, and database connections, and can scaffold new modules/actions/plugins/connections.

Quick start

composer install

# Bundle the Quiote docs into MCP resources (one-time, or whenever the docs change).
php bin/quiote-assistant mcp:docs:sync --source=/path/to/quioteframework.github.io/src/content/docs

# Serve over stdio — knowledge tools only (no project to introspect yet).
php bin/quiote-assistant

# ...or point it at a Quiote app to unlock the project-aware tools too:
php bin/quiote-assistant --target-app-dir=/path/to/your/project

# ...or point it straight at a cassette store, to read production cassettes with
# no checkout of the recording app at all (see "Reading production cassettes"):
php bin/quiote-assistant \
  --cassette-store=azure-blob \
  --azure-account=prodcassettes \
  --azure-container=quiote-cassettes \
  --azure-env=production

The server speaks MCP over stdio by default — it's meant to be launched by a client as a subprocess, not run standalone in a terminal for interactive use.

Connect a client

All of these launch bin/quiote-assistant as a stdio subprocess. Add --target-app-dir=/path/to/your/project to any command/args list below to enable the project-aware tools.

If you omit --target-app-dir, it falls back to Quiote\Console\AppDirResolver -- the same discovery vendor/bin/quiote itself uses: a .quiote.json marker ({"app_dir": "relative/or/absolute/path"}) found by walking up from the client's launch directory, or (failing that) the nearest ancestor directory containing Config/settings.{php,xml,yaml,yml}. This means a client that launches the server with the workspace root as its working directory can skip the explicit flag entirely if that workspace already has (or you add) a .quiote.json -- handy when the app directory isn't the workspace root itself (e.g. {"app_dir": "src/MyApp"}) and you'd otherwise have to hardcode and maintain that absolute path in the client config by hand. An explicit --target-app-dir always takes precedence over this discovery.

Claude Code
claude mcp add quiote -- php /abs/path/to/quiote-mcp-assistant/bin/quiote-assistant --target-app-dir=.

...or a project-scoped .mcp.json:

{ "mcpServers": {
    "quiote": {
      "command": "php",
      "args": ["/abs/path/to/quiote-mcp-assistant/bin/quiote-assistant", "--target-app-dir=/path/to/your/project"]
    }
} }
Cursor

.cursor/mcp.json (project-scoped) or the global ~/.cursor/mcp.json:

{ "mcpServers": {
    "quiote": {
      "command": "php",
      "args": ["/abs/path/to/quiote-mcp-assistant/bin/quiote-assistant", "--target-app-dir=/path/to/your/project"]
    }
} }
GitHub Copilot (VS Code Chat)

.vscode/mcp.json:

{ "servers": {
    "quiote": {
      "type": "stdio",
      "command": "php",
      "args": ["/abs/path/to/quiote-mcp-assistant/bin/quiote-assistant", "--target-app-dir=/path/to/your/project"]
    }
} }

Copilot Chat only calls MCP tools in Agent mode — switch the mode dropdown next to the chat input before asking it to do anything, otherwise it'll fall back to its own built-in file-search tools and ignore the server entirely.

GitHub Copilot CLI

A project-scoped .mcp.json (same shape as Claude Code's, above) is picked up automatically. Run /mcp inside the CLI to confirm the server shows as connected and see its tool count.

Over HTTP instead (shared/team deployments)

{ "mcpServers": {
    "quiote": {
      "type": "http",
      "url": "https://your-host/mcp",
      "headers": { "Authorization": "Bearer YOUR_TOKEN" }
    }
} }

See HTTP transport below for how to actually run it that way.

Run it as a standalone PHAR

No PHP source checkout needed at runtime — build once, ship one file:

bin/build-phar
php build/quiote-assistant.phar --target-app-dir=/path/to/your/project

Use the built .phar in place of bin/quiote-assistant in any of the client configs above. Run mcp:docs:sync before building — the docs are baked into the archive at build time, not regenerated at runtime.

Run it via Docker

Bundles its own PHP 8.5 + every extension needed, so there's nothing to install locally at all — useful if getting a matching local PHP set up (e.g. under WSL) is more trouble than it's worth:

docker build -t quiote-assistant .

# Knowledge tools only:
docker run -i --rm quiote-assistant

# Project-aware tools too -- mount the project at a fixed path and point
# --target-app-dir at that path (not the host path, which doesn't exist
# inside the container). --user avoids scaffolded files coming out
# root-owned on the host.
docker run -i --rm \
  --user "$(id -u):$(id -g)" \
  -v /path/to/your/project:/target \
  quiote-assistant --target-app-dir=/target

Client config is the same shape as everywhere above, just with docker as the command:

{ "mcpServers": {
    "quiote": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm", "--user", "1000:1000",
        "-v", "/path/to/your/project:/target",
        "ghcr.io/quioteframework/quiote-mcp-assistant:latest",
        "--target-app-dir=/target"
      ]
    }
} }

(Replace 1000:1000 with your actual uid:gid — client configs can't run $(id -u) for you.) Pre-built images are published on tagged releases to ghcr.io/quioteframework/quiote-mcp-assistant, tagged both :latest and :vX.Y.Z.

The image only ships pdo_sqlite (bundled with the base PHP image) as a PDO driver, not pdo_mysql/pdo_pgsql — none of the project-aware tools open a real database connection (list_db_connections only parses databases.xml for metadata, run_console's whitelist never touches the DB, and Database::connect() is lazy on first query). Quiote itself doesn't stop an app from eagerly connecting somewhere in its own bootstrap, though — it's a "do whatever you want" framework, not a walled garden — so if your target app's bootstrap path is unusual enough to open a MySQL/Postgres connection eagerly, add the matching docker-php-ext-install line to the Dockerfile yourself.

HTTP transport

For a team to share one running instance instead of one subprocess per client, mcp.transports includes 'http' (app/Config/settings.php), which registers POST /mcp on the app's normal PSR-7 front controller. Bearer auth is on by default and safe when unconfigured — an unset/empty mcp.auth_token rejects every request rather than silently disabling auth:

QUIOTE_ASSISTANT_MCP_TOKEN=$(openssl rand -hex 32) php -S 0.0.0.0:8080 app/pub/index.php

Note: MCP's HTTP mode is session-based — the Mcp-Session-Id header from initialize must be sent on every later request, and that session lives in PHP process memory. A plain php -S / PHP-FPM / CGI deployment starts a fresh process per request, so sessions won't survive between calls. A real deployment needs a persistent-worker runtime (e.g. FrankenPHP worker mode) or a shared session store.

What it exposes

Resources

Every bundled Quiote doc is exposed as one MCP resource, readable with resources/read (e.g. quiote-docs://basics/routing, quiote-docs://architecture/plugins, …). Use search_docs to find the right URI rather than guessing it.

Knowledge tools (always available)

Tool Description
search_docs(query, limit?) Ranked full-text search across the docs, returning excerpts + the resource URI to cite.
get_convention(topic) A concise convention card. Topics: actions, routing, config, di, plugins, database, validation, mcp.
get_recipe(task) Step-by-step instructions + runnable code for a concrete task. Tasks: read-only-action, multi-output-view, form-action, add-plugin, add-database-connection, expose-action-as-tool, register-mcp-tool.
describe_symbol(symbol) Reflection-based signature + docblock for a Quiote\* class/interface/trait/enum, or Class::method for one method.
list_api(namespace?, limit?) Browse the Quiote\* namespace tree; omit namespace to list top-level namespaces.

Project-aware tools (only when launched with --target-app-dir)

Read-only:

Tool Description
project_info() Environment, default context, enabled plugins, module list.
overview() Routes + modules + Action/View/Template triads + diagnostics + shadowed-config info, all from one app bootstrap. Prefer this over calling list_routes/list_modules/describe_action separately when you need more than one of them.
diagnostics() Every problem this app can find in one call: routing (missing action class, duplicate route), triad (missing view/template), and config (syntax/semantic/schema errors, shadowed configs) — one flat list sharing a single {severity, code, message, file, line, ...} shape.
list_routes(module?, action?) Every route the target app's live RouteCollection resolves with (attribute-routed and programmatic), plus the action class file/line per route and a top-level diagnostics array. Filter server-side to one module and/or action (e.g. module: "Library") instead of fetching everything on a large app.
describe_action(action) Verbs (each with its validator-derived input schema and source line), credentials, default view, and the resolved viewFile/templateFile for "Module.Action".
list_db_connections() Adapter class + parameter names only — never parameter values (DSNs/credentials are never disclosed).
list_plugins() Plugins registered during the target app's bootstrap.
list_modules() Module names discovered under the target app's module directory.
read_config(key?) One setting, restricted to an explicit allowlist (never secrets like mcp.auth_token). Omit key to see the allowlist.
validate_config(key?) Validates the target app's config files — syntax (per-format, with line numbers), semantic (the real config handler's own compilation), and array-shape schema checks — format-agnostically across PHP/YAML/XML. Omit key to validate every known config type (settings, factories, databases, output_types, rbac_definitions, translation, plugins, middleware).

Diagnosing a production failure (requires quioteframework/replay installed and a cassette store configured in the target app — see its own advanced/record-replay doc): list_failed_requests finds a candidate id, describe_cassette/cassette_section reads it, replay_cassette confirms the repro locally, emit_replay_test commits it as a regression test. Every one of them resolves an id the same way: the local cache, then the target app's configured store, then (given a key/date/hour hint, or none at all if the target app has a log-analytics index configured) the cassette-index chain — caching a store/index hit locally so a repeat lookup needs no network.

Tool Description
list_failed_requests(since?, limit?) Candidate cassette ids for "what broke": every recorded cassette whose response was a 5xx, or whose recorder trigger was error, newest first.
describe_cassette(id, key?, date?, hour?, include_bodies?) The redacted analysis payload for one cassette — request/resolved route/session/user/effects/response/exception, bodies and effect rows excerpted by default (pass include_bodies for the full content).
cassette_section(id, section, key?, date?, hour?, include_bodies?) One top-level section of a cassette (meta, request, resolved, session, user, effects, response, exception, log) without paying for the whole describe_cassette payload.
replay_cassette(id, key?, date?, hour?, force?) Re-runs a cassette against the target app's real pipeline and returns the response diff + drift report. Refuses to run unless the target app has replay.allow_live=true, and refuses a non-idempotent recorded method unless force=true.
emit_replay_test(id, key?, date?, hour?, expect_fixed?) Writes a committed PHPUnit regression test (plus a copy of the cassette) under the target app's replay.tests_path. expect_fixed emits an inverted markTestIncomplete skeleton instead of asserting the recorded (buggy) response, for committing a test before the fix lands.

Scaffolding + console (dry_run defaults to true on every write tool — it returns a diff and writes nothing until you pass dry_run=false; none of them ever overwrite an existing file):

Tool Description
scaffold_module(module, dry_run?) New module skeleton — an Index action + view + template.
scaffold_action(module, action, verbs?, formats?, dry_run?) New action + view + template(s), with a #[Route] attribute. verbs is one or more of read/write/update/remove. formats (default ["html"]) is one or more output types the view should serve, e.g. ["html", "json"] — each gets its own execute<Format>() method; a format not yet declared in Config/output_types.xml is reported back as a ready-to-paste snippet.
scaffold_plugin(name, dry_run?) New plugin class. Never auto-registers it in Config/settings.* — the response tells you the one line to add.
scaffold_db_connection(name, driver?, dry_run?) New Config/databases.xml if one doesn't exist yet, otherwise a ready-to-paste snippet. driver is one of pdo/eloquent/doctrine/doctrine_dbal/cycle.
run_console(command, args?) Runs one of the target app's own console commands, restricted to a non-destructive whitelist (about, routes:list, plugins:list, middleware:list, cache:warmup); unlisted commands or options are refused.

Prompts

Parameterized templates that stitch together the right convention card + recipe:

Prompt Description
new-module Guidance + a checklist for adding a new module.
add-action Guidance for adding a new action (verbs/validators/view) to a module.
add-service Guidance for adding a DI-resolved service/model.
add-plugin Guidance for writing a plugin that contributes via PluginRegistrar.
add-db-connection Guidance for declaring a new database connection.
expose-mcp-tool Guidance for exposing an existing #[Route] action as an MCP tool.

Reading production cassettes

"Read Quiote cassette SUX2020 and analyze the issue" needs one thing: a cassette store the server can read. There are two ways to give it one, and they answer different questions.

Point it at the app that recorded them

php bin/quiote-assistant --target-app-dir=/path/to/production/checkout

The cassette tools run probe.php against that checkout, which bootstraps it for real — its config, its plugins, its store. This is the route to use when the point is to reason about the application, and it is the only route that can replay_cassette (which dispatches the recorded request through the app's own pipeline) or emit_replay_test (which writes a test file into the app's own tree).

The checkout needs, in its own config:

// Config/plugins.php — order no longer matters, but the package must be installed.
array('class' => \Quiote\Replay\Store\Azure\ReplayAzurePlugin::class),
array('class' => \Quiote\Replay\ReplayPlugin::class),

// Config/settings.php
'replay.store'                   => 'azure-blob',
'replay.store.azure.account'     => 'prodcassettes',
'replay.store.azure.container'   => 'quiote-cassettes',
'replay.store.azure.auth'        => 'cli',   // reuse your own `az login` identity

plus composer require quioteframework/replay-azure and a PSR-18 Psr\Http\Client\ClientInterface bound in its container — no Azure-backed package binds one for you, deliberately.

Point it straight at the container

php bin/quiote-assistant \
  --cassette-store=azure-blob \
  --azure-account=prodcassettes \
  --azure-container=quiote-cassettes \
  --azure-env=production

No checkout, no target app. list_failed_requests, describe_cassette and cassette_section run in-process against that container — reading a cassette needs a store, not a source tree. replay_cassette and emit_replay_test stay unavailable, because neither is expressible without the application.

Every option also reads an environment variable (QUIOTE_ASSISTANT_AZURE_ACCOUNT and friends), which is usually easier to manage from an MCP client config. They are applied before bootstrap and marked readonly, so they are never baked into the settings cache and never lose to app/Config/settings.php.

Option Meaning
--cassette-store azure-blob, pdo, or file
--cassette-path Directory, for --cassette-store=file
--azure-account Storage account name
--azure-container Blob container
--azure-prefix Key prefix (default quiote-cassettes)
--azure-auth cli (default), workload_identity, chain, or shared_key
--azure-env Environment segment of the key — the environment that recorded the cassettes
--azure-lookback-hours How far back a bare id is probed (default 48)

Credentials

--azure-auth=cli is the default and shells out to az account get-access-token, so the identity is whatever you already authenticated with az login and no account key is handed to the server. You need a role that can read the container — Storage Blob Data Reader is enough.

--azure-env matters and is easy to miss: a cassette's key is {prefix}/{env}/{yyyy}/{mm}/{dd}/{hh}/{id}.qcast, and {env} is the environment of the deployment that wrote it. Reading production cassettes from a laptop means passing --azure-env=production — the server's own environment is not it, and cannot be borrowed (core.environment is locked readonly once bootstrapped).

Finding an id

A bare id resolves only if the cassette is inside --azure-lookback-hours of now, because the store probes backward hour by hour from the deterministic key. For anything older, give the tools a hint — every cassette tool takes date (YYYY-MM-DD), hour (0023) and key:

  • date narrows to one day's partition — one request per hour bucket.
  • key is the exact object key, which the recorder writes into its own pointer log line (cassette_key), so pasting it from your log viewer is the fastest path and needs no scan.

A Log Analytics workspace (replay.index.log_analytics.workspace_id) removes the need for a hint entirely by looking that pointer line up for you; without one, describe_cassette on an old id and no hint fails with a message saying exactly that.

Running tests

composer test       # PHPUnit: unit tests (pure logic) + integration tests (self-bootstrapped app)
composer phpstan     # static analysis, level 9

tests/Unit/ covers pure logic in isolation (doc search ranking, scaffold code generation -- every generated file is linted with a real php -l, not just string-matched -- the console command allowlist, the scaffold-writer's never-overwrite guarantee, framework reflection, and the hand-authored convention cards/recipes). tests/Integration/ bootstraps this app itself (the same self-targeting tools/mcp-smoke-client.php already relies on) to test the introspection capabilities against a real, live Context rather than a mock. Both suites test failure paths deliberately, not just the happy path -- rejected/malformed input, permission failures, unknown symbols, and the security-critical read_config allowlist refusal are all exercised for real, not just asserted never to happen.

composer test requires a coverage driver (PCOV or Xdebug) to also emit a coverage report -- without one, tests still run, just without the report. Output goes to build/coverage/ (html/index.html for a browsable report, clover.xml for tooling); a summary also prints to the terminal. The release workflow uploads this as a build artifact on every run.

Verifying a local build

php tools/mcp-smoke-client.php                              # knowledge + project-aware tools, self-targeting this repo's app/
php tools/mcp-smoke-client-scaffold.php /path/to/scratch/app # scaffolding + run_console, against a throwaway app (never this repo's app/)
php tools/mcp-http-smoke-client.php                          # HTTP transport: auth + a full session-based conversation

Each drives the server as a real MCP client would (initializetools/list/tools/call). The PHAR is verified the same way, just pointed at the built archive instead of bin/quiote-assistant.

Further reading

The doc comments throughout app/Mcp/ cover the design decisions and internals (why project-aware tools run in an isolated subprocess, how the PHAR handles read-only-archive constraints, etc.) if you're extending this server rather than just using it.