The AI assistant (MCP server)
The Quiote Assistant MCP is a Model Context Protocol server that gives an AI coding agent — Claude Code, Cursor, Copilot, or any MCP-capable client — authoritative, current knowledge of the Quiote framework, and, when pointed at a real app, the ability to inspect and scaffold it. Instead of relying on whatever a model happened to memorise (and Quiote is far too new and niche for that), the agent reads the real docs, searches them, reflects on the actual framework source, and can introspect or extend the project it’s working in.
It is, itself, a Quiote application. It dogfoods the framework’s own app-as-MCP-server capability — the same Quiote\Mcp\McpPlugin any Quiote app can turn on — so the assistant is both a useful tool and the reference example of building an MCP server with Quiote.
What it exposes
Section titled “What it exposes”Resources — the docs, one URI each
Section titled “Resources — the docs, one URI each”Every page of this documentation site is bundled as an MCP resource under a quiote-docs:// URI (49 at last sync), readable with the standard resources/read call:
quiote-docs://basics/routingquiote-docs://architecture/pluginsquiote-docs://getting-started/your-first-app…The agent can list them and read any page verbatim as text/markdown, so answers are grounded in — and can cite — the authoritative source rather than a paraphrase.
Knowledge tools — always available
Section titled “Knowledge tools — always available”Five tools work with no target app at all; they answer questions about the framework itself.
search_docs(query, limit?) — a ranked full-text search across the whole documentation set. Returns scored excerpts, each with the quiote-docs:// URI to read in full and cite. limit is optional (default 5, max 20).
// search_docs({ "query": "define a route", "limit": 3 }){ "query": "define a route", "count": 3, "results": [ { "uri": "quiote-docs://basics/routing", "title": "Routing", "score": 85, "excerpt": "# Routing > Declaring routes with a Routing subclass and with #[Route] attributes…" } ]}get_convention(topic) — a concise, task-oriented convention card: the crisp, hand-authored notes the prose docs don’t always state in one place. Topics: actions, routing, config, di, plugins, database, validation, mcp.
// get_convention({ "topic": "actions" }){ "topic": "actions", "title": "Actions & the verb model", "body": "An action is one class per action, extending `Quiote\\Action\\Action`…" }get_recipe(task) — step-by-step instructions with runnable code for a concrete task, rather than a convention summary. task is an enum: new-project, read-only-action, multi-output-view, form-action, add-plugin, add-database-connection, throttle-login, expose-action-as-tool, register-mcp-tool.
// get_recipe({ "task": "add-plugin" }){ "task": "add-plugin", "title": "Write and register a plugin", "steps": [ { "description": "Implement PluginInterface -- just a register()... #[Plugin(name: ...)] is required, not optional...", "code": "#[Plugin(name: 'health')]\nfinal class HealthPlugin implements PluginInterface { ... }" }, { "description": "Activate it via Config/plugins.php -- NOT a \"plugins\" key inside settings.php. Each entry is {class, enabled?}...", "code": "return [ ['class' => \\App\\Plugin\\HealthPlugin::class] ];" } ]}Note how much of the step text is spent on the failure modes rather than the happy path — that #[Plugin] is mandatory for class-string activation, and that the settings.php route only incidentally works. That is deliberate: the recipes carry the caveats an agent would otherwise have to infer from a silent failure.
describe_symbol(symbol) — a reflection-based signature and docblock for a real Quiote\* class, interface, trait, or enum (optionally Class::method for one method), so the answer never drifts from the installed code. Rejects anything outside the Quiote\ namespace.
// describe_symbol({ "symbol": "Quiote\\Action\\Action::getCredentials" }){ "symbol": "Quiote\\Action\\Action::getCredentials", "fqcn": "Quiote\\Action\\Action", "method": { "name": "getCredentials", "static": false, "abstract": false, "returnType": null, "parameters": [], "summary": "Retrieve the credential required to access this action.", "attributes": [] }}list_api(namespace?, limit?) — browse the Quiote\* namespace tree. Called with no arguments it lists top-level namespaces; called with one of those, it lists that namespace’s classes (each with a one-line summary), ready to feed into describe_symbol for full detail. limit defaults to 50, max 200.
// list_api({ "namespace": "Quiote\\Mcp", "limit": 2 }){ "namespace": "Quiote\\Mcp", "total": 14, "count": 2, "truncated": true, "classes": [ { "fqcn": "Quiote\\Mcp\\Auth\\McpAuthenticatorInterface", "kind": "interface", "summary": "Validates the bearer token presented to the MCP HTTP endpoint…" }, { "fqcn": "Quiote\\Mcp\\Bridge\\ActionToolAdapter", "kind": "class", "summary": "The actions-as-tools bridge (the headline feature)…" } ]}Both describe_symbol and list_api are backed by real ReflectionClass/ReflectionMethod introspection against the actually-installed framework code (found by reading Composer’s own PSR-4 prefix map, not a hardcoded list) — not generated or static content. If the framework changes, these answers change with it.
Project-aware tools — when launched against a real app
Section titled “Project-aware tools — when launched against a real app”Launch the assistant with --target-app-dir=/path/to/your/app and fifteen more tools appear, letting the agent inspect and extend that app rather than just answer questions about the framework in the abstract.
Read-only introspection:
| Tool | Returns |
|---|---|
project_info | Basic app identity — environment, default context, enabled plugins, and module list, read live from the bootstrapped app. |
overview | Routes, modules, Action/View/Template triads, diagnostics, and shadowed-config info, all from one app bootstrap — prefer it over calling the individual tools separately. |
diagnostics | Every problem the app can find in one call — routing (missing action class, duplicate route name/path), triad (missing view/template), and config errors — as one flat list sharing a {severity, code, message, file, line, …} shape. |
list_routes | Every route the app actually resolves with, read live from its RouteCollection (optionally filtered by module/action). |
describe_action | Details of one action — verbs, validator-derived input schema per verb, required credentials, default view. |
list_db_connections | Configured databases connections — adapter class and parameter names only, never credential values. |
list_plugins | Plugins actually registered during the app’s bootstrap. |
list_modules | Modules under the app’s module directories. |
read_config | A single, allowlisted settings key (secrets like mcp.auth_token are refused, with the allowlist returned instead). |
validate_config | Validation of the app’s config files — syntax (per-format, with line numbers), semantic, and array-shape schema checks, format-agnostic across PHP/YAML/XML (omit key to validate every known config type). |
Scaffolding (write) tools — scaffold_module, scaffold_action, scaffold_plugin, scaffold_db_connection generate real files in the target app; run_console invokes a bin/quiote console command. All default to dry_run: true and never overwrite an existing file — the agent (or you) has to explicitly opt into writing.
scaffold_action(module, action, verbs?, formats?, dry_run?) takes a formats array (default ["html"]) — each requested format gets its own execute<Format>() method on the generated action; "html" also gets a template, others (e.g. "json") return their body directly. A format not yet declared in your output_types config is reported back as a ready-to-paste config snippet rather than being written for you.
Prompts — scaffolding recipes
Section titled “Prompts — scaffolding recipes”Parameterized prompt templates that stitch the right convention card together with a step-by-step recipe, so the agent starts a task with the correct conventions already in hand:
| Prompt | Guides the agent through… |
|---|---|
new-module | Adding a new module to an app |
add-action | Adding an action (verbs, validators, view) to a module |
add-service | Adding a DI-resolved service or model |
add-plugin | Writing a plugin that contributes via PluginRegistrar |
add-db-connection | Declaring a new database connection |
expose-mcp-tool | Exposing an existing #[Route] action as an MCP tool |
Installing and running it
Section titled “Installing and running it”The server is a standalone Quiote app (PHP 8.5+), built on the PHP MCP SDK (mcp/sdk, currently pinned to ^0.6.0) plus quioteframework/mcp. It lives in its own repository and supports two transports, a self-contained binary, and a Docker image.
-
Install its dependencies:
Terminal window cd quiote-mcp-assistantcomposer install -
Bundle the docs into MCP resources. The docs are generated, not hand-maintained — this command reads a Starlight docs checkout, strips the component markup, and writes the resource files plus their manifest:
Terminal window php bin/quiote-assistant mcp:docs:sync \--source=/path/to/quioteframework.github.io/src/content/docs -
Serve it. Over stdio (what a client launches as a subprocess):
Terminal window php bin/quiote-assistantPoint it at a real app to get the project-aware tools:
Terminal window php bin/quiote-assistant --target-app-dir=/path/to/your/app--target-app-dircan be omitted if the launch directory (or an ancestor of it) contains a.quiote.jsonmarker file ({"app_dir": "relative/or/absolute/path"}) or aConfig/settings.{php,xml,yaml,yml}— the sameQuiote\Console\AppDirResolverdiscoveryvendor/bin/quioteitself uses. An explicit--target-app-diralways wins. The launcher also rejects a bare--app-dirflag with a “did you mean--target-app-dir?” error, since that’s the flag name for the target app’s ownbin/quioteinvocations, not the assistant’s.
HTTP transport
Section titled “HTTP transport”The assistant can also serve Streamable HTTP instead of (or alongside) stdio, useful for a shared/remote deployment rather than one subprocess per client.
- Sessions —
Mcp-Session-Idis issued on the first call and must be repeated on every subsequent one. Sessions live in PHP process memory, so a plainphp -S/ classic PHP-FPM deployment (a fresh process per request) won’t retain them — run it under a persistent-worker runtime such as FrankenPHP worker mode. - Bearer auth, safe by default — set
mcp.auth_tokenin the app’s settings, typically from an environment variable:Like any setting, this key can also live inapp/Config/settings.php 'mcp.auth_token' => getenv('QUIOTE_ASSISTANT_MCP_TOKEN') ?: null,app/Config/settings.yamlorapp/Config/settings.xml(see Configuration); thegetenv()read shown here is a PHP idiom the other formats don’t offer. With no token configured,Quiote\Mcp\Auth\StaticTokenAuthenticatorrejects every HTTP request — there’s no accidental “wide open” default. For a trusted network or a reverse proxy that already authenticates,mcp.auth = 'none'is the explicit opt-out — a deliberate, last-resort override. The bearer check runs inMcpAuthMiddlewareahead of the endpoint itself; see how an MCP HTTP request is handled for the exact pipeline order.
Example run:
QUIOTE_ASSISTANT_MCP_TOKEN=$(openssl rand -hex 32) php -S 0.0.0.0:8080 app/pub/index.phpA standalone PHAR
Section titled “A standalone PHAR”bin/build-phar packages the assistant into a single build/quiote-assistant.phar, so a client can be pointed at one file instead of a checkout plus composer install.
Docker
Section titled “Docker”A Dockerfile (PHP 8.5-cli-alpine, docker-php-ext-installing intl and xsl — pdo_sqlite and the rest of the base extensions ship preinstalled in the base image, not installed here — --no-dev composer install) packages the assistant as an image, published to ghcr.io/quioteframework/quiote-mcp-assistant on tagged releases (:latest and :vX.Y.Z). It deliberately adds no pdo_mysql/pdo_pgsql — none of the project-aware tools open a real database connection, so no extra DB driver is needed in the image.
docker build -t quiote-assistant .docker run -i --rm quiote-assistant # knowledge tools only
docker run -i --rm --user "$(id -u):$(id -g)" \ -v /path/to/your/project:/target \ quiote-assistant --target-app-dir=/target # project-aware toolsRegistering with a client
Section titled “Registering with a client”Point your MCP client at the launcher — a PHP invocation, the PHAR, or the Docker image. The mcpServers/servers block shape is close to universal, but the exact file and a couple of details differ per client:
claude mcp add quiote -- php /abs/path/to/quiote-mcp-assistant/bin/quiote-assistant --target-app-dir=.Or drop the equivalent block into a project-scoped .mcp.json:
{ "mcpServers": { "quiote": { "command": "php", "args": [ "/abs/path/to/quiote-mcp-assistant/bin/quiote-assistant", "--target-app-dir=/abs/path/to/your/app" ] } }}The same mcpServers shape, in .cursor/mcp.json (project-scoped) or ~/.cursor/mcp.json (global).
.vscode/mcp.json, under a servers key with an explicit "type": "stdio":
{ "servers": { "quiote": { "type": "stdio", "command": "php", "args": ["/abs/path/to/quiote-mcp-assistant/bin/quiote-assistant", "--target-app-dir=."] } }}Copilot Chat only calls MCP tools in Agent mode — plain chat/edit mode ignores registered servers.
Picks up a project-scoped .mcp.json automatically; verify with /mcp inside the CLI session.
{ "mcpServers": { "quiote": { "type": "http", "url": "https://your-host:8080/mcp", "headers": { "Authorization": "Bearer YOUR_TOKEN" } } }}For the Docker image, command/args become "docker" and ["run", "-i", "--rm", "--user", "1000:1000", "-v", "/path/to/your/project:/target", "ghcr.io/quioteframework/quiote-mcp-assistant:latest", "--target-app-dir=/target"].
Omit --target-app-dir if you only want the knowledge tools. Once registered, tools appear to the agent namespaced by the client (e.g. mcp__quiote__describe_symbol), the docs as readable resources, and the recipes as prompts.
How it’s built
Section titled “How it’s built”This is the part worth studying if you’re building your own MCP server on Quiote — the assistant is deliberately a plain app with nothing special about it.
It’s turned on by two settings. mcp.enabled = true, plus both Quiote\Mcp\McpPlugin (which publishes the mcp.* settings and registers the mcp:serve transport) and the app’s own AssistantPlugin, both listed in Config/plugins.php:
return [ ['class' => \Quiote\Mcp\McpPlugin::class, 'enabled' => true], ['class' => \QuioteMcpAssistant\Mcp\AssistantPlugin::class, 'enabled' => true],];'mcp.enabled' => true,'mcp.transports' => ['stdio', 'http'],'mcp.server_name' => 'quiote-assistant',Every capability is registered manually, directly against Quiote\Mcp\McpCatalog. There’s no attribute discovery for plain handler classes (only #[Route] actions carrying #[McpTool] are scanned) — and note that PluginRegistrar::mcpTool()/mcpResource()/mcpPrompt() convenience methods, which used to be the documented seam here, have since been removed from the framework core. AssistantPlugin reproduces that convenience itself with small private wrapper methods, mcpResource()/mcpTool()/mcpPrompt(), that forward to McpCatalog::addResource()/addTool()/addPrompt():
public function register(PluginRegistrar $registrar): void{ // …one mcpResource() per bundled doc, from the generated manifest… $this->mcpResource( handlerFqcn: DocsResource::class, uri: 'quiote-docs://basics/routing', method: 'read', name: 'basics_routing', description: 'Routing — Declaring routes…', mimeType: 'text/markdown', );
$this->mcpTool( handlerFqcn: DescribeSymbolTool::class, method: 'describe', name: 'describe_symbol', description: 'Reflection-based signature + docblock for a Quiote framework symbol…', inputSchema: [/* JSON Schema for { symbol } */], );}The private wrappers just combine handlerFqcn + method into the [Class::class, 'method'] callable array McpCatalog::addResource()/addTool() actually take. If you’re writing your own MCP-serving plugin today, go straight to McpCatalog rather than looking for a PluginRegistrar method — see Plugins and extensibility for the rest of the seams that are still on PluginRegistrar.
Keeping the docs current
Section titled “Keeping the docs current”Because the resources are generated by mcp:docs:sync, the assistant’s knowledge is only as fresh as the last sync. Re-run it whenever this documentation changes — it re-reads the Starlight source, re-strips the component markup, and rewrites the bundled resources and their manifest:
php bin/quiote-assistant mcp:docs:sync --source=/path/to/quioteframework.github.io/src/content/docsIf the sync has never been run, the server still boots — it simply exposes no doc resources (the tools and prompts still work) rather than failing.
Verifying it works
Section titled “Verifying it works”The repo ships three real MCP client smoke tests, each launching the server as a real subprocess (or over HTTP) and driving a full JSON-RPC conversation:
php tools/mcp-smoke-client.php # stdio: full read-only surface, self-targeted at app/php tools/mcp-smoke-client-scaffold.php /path/to/scratch/app # scaffolding writes + run_console, against a throwaway appphp tools/mcp-http-smoke-client.php # HTTP transport: sessions + bearer authThe scaffold smoke test takes the scratch app’s path as an explicit argument — never point it at this repo’s own app/. Run them before pointing an agent at the server — they cover every tool, resource, and prompt, plus the project-aware and scaffolding tools against a real (if disposable) app.