Skip to content

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.

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/routing
quiote-docs://architecture/plugins
quiote-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.

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…" }
]
}

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:

ToolReturns
project_infoBasic app identity — environment, default context, enabled plugins, and module list, read live from the bootstrapped app.
overviewRoutes, modules, Action/View/Template triads, diagnostics, and shadowed-config info, all from one app bootstrap — prefer it over calling the individual tools separately.
diagnosticsEvery 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_routesEvery route the app actually resolves with, read live from its RouteCollection (optionally filtered by module/action).
describe_actionDetails of one action — verbs, validator-derived input schema per verb, required credentials, default view.
list_db_connectionsConfigured databases connections — adapter class and parameter names only, never credential values.
list_pluginsPlugins actually registered during the app’s bootstrap.
list_modulesModules under the app’s module directories.
read_configA single, allowlisted settings key (secrets like mcp.auth_token are refused, with the allowlist returned instead).
validate_configValidation 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) toolsscaffold_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.

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:

PromptGuides the agent through…
new-moduleAdding a new module to an app
add-actionAdding an action (verbs, validators, view) to a module
add-serviceAdding a DI-resolved service or model
add-pluginWriting a plugin that contributes via PluginRegistrar
add-db-connectionDeclaring a new database connection
expose-mcp-toolExposing an existing #[Route] action as an MCP tool

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.

  1. Install its dependencies:

    Terminal window
    cd quiote-mcp-assistant
    composer install
  2. 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
  3. Serve it. Over stdio (what a client launches as a subprocess):

    Terminal window
    php bin/quiote-assistant

    Point 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-dir can be omitted if the launch directory (or an ancestor of it) contains a .quiote.json marker file ({"app_dir": "relative/or/absolute/path"}) or a Config/settings.{php,xml,yaml,yml} — the same Quiote\Console\AppDirResolver discovery vendor/bin/quiote itself uses. An explicit --target-app-dir always wins. The launcher also rejects a bare --app-dir flag with a “did you mean --target-app-dir?” error, since that’s the flag name for the target app’s own bin/quiote invocations, not the assistant’s.

The assistant can also serve Streamable HTTP instead of (or alongside) stdio, useful for a shared/remote deployment rather than one subprocess per client.

  • SessionsMcp-Session-Id is issued on the first call and must be repeated on every subsequent one. Sessions live in PHP process memory, so a plain php -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_token in the app’s settings, typically from an environment variable:
    app/Config/settings.php
    'mcp.auth_token' => getenv('QUIOTE_ASSISTANT_MCP_TOKEN') ?: null,
    Like any setting, this key can also live in app/Config/settings.yaml or app/Config/settings.xml (see Configuration); the getenv() read shown here is a PHP idiom the other formats don’t offer. With no token configured, Quiote\Mcp\Auth\StaticTokenAuthenticator rejects 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 in McpAuthMiddleware ahead of the endpoint itself; see how an MCP HTTP request is handled for the exact pipeline order.

Example run:

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

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.

A Dockerfile (PHP 8.5-cli-alpine, docker-php-ext-installing intl and xslpdo_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.

Terminal window
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 tools

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:

Terminal window
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"
]
}
}
}

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.

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:

app/Config/plugins.php
return [
['class' => \Quiote\Mcp\McpPlugin::class, 'enabled' => true],
['class' => \QuioteMcpAssistant\Mcp\AssistantPlugin::class, 'enabled' => true],
];
app/Config/settings.php
'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.

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:

Terminal window
php bin/quiote-assistant mcp:docs:sync --source=/path/to/quioteframework.github.io/src/content/docs

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

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:

Terminal window
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 app
php tools/mcp-http-smoke-client.php # HTTP transport: sessions + bearer auth

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