Index

MCPController

MMO\core\controllers · 1 symbol · generated 5 Oct 2026

class

MCPController

MCPController.php:50

MCPController — a Model Context Protocol (https://modelcontextprotocol.io) server that exposes the MMOFramework itself to any MCP-compatible AI agent (Claude, or any other client speaking the protocol).

class MMO\core\controllers\MCPController extends MMO\core\classes\aController
Namespace
MMO\core\controllers
Extends
aController
## Route `mcp.json` `POST|mcp.json` is the single JSON-RPC 2.0 endpoint (the request/response flavour of the "Streamable HTTP" transport — no SSE stream is opened, so `GET|mcp.json` answers 405). Point any MCP client at this url. Supported JSON-RPC methods: `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, `prompts/list`, `prompts/get`, plus the notifications `notifications/initialized` and `notifications/cancelled` (accepted and ignored — this server keeps no session state). `server/discover` is kept as a legacy alias for `initialize` for whatever already targets it. JSON-RPC batching (an array of requests in one body) is refused with an "invalid request" error, matching MCP protocol revision 2025-06-18, which removed batching support. ## What this controller actually gives an agent - `initialize`'s `instructions` (mirrored as the `mmoframework://guide` resource, [`self::GUIDE`](https://mcp.mmoframework.fr/documentation/mmo.core/php/controllers/MCPController.md#mcpcontroller-guide)) is a full, self-contained guide to coding with MMOFramework: singletons/DI, entities (the ORM layer), controllers/routing, schemas, events, and the namespaced-function convention. - Tools let an agent introspect *this specific application* — its actually-registered entities, routes, classes and namespaced helper functions — instead of guessing from training data, plus scaffold new entities/controllers that follow the framework's exact conventions. - Resources expose every `*.md` file shipped by every included package's `documentations` folder (see `mmo.json` / `PackageManifest::$documentations`), so any package built on top of mmo.core — mmo.client, mmo.oauth, mmo.pdo, ... — contributes its own docs automatically, with no wiring needed here. `list_packages` surfaces each included package's own `PackageManifest` "showcase" metadata (title, description, status, dependencies), so an agent can discover what libraries this application actually has available before assuming a capability doesn't exist. With [`$discovered`](https://mcp.mmoframework.fr/documentation/mmo.core/php/controllers/MCPController.md#mcpcontroller-discovered), both also cover every package only discovered. - Prompts (`explain_framework`, `create_entity`, `create_controller`) are ready-made, parameterized instructions a client can surface as slash-commands. See `docs/MCP.md` for the full write-up of how this is put together and why.

Index

DOC_INDEX
ERROR_INTERNAL
ERROR_INVALID_PARAMS
ERROR_INVALID_REQUEST
ERROR_METHOD_NOT_FOUND
ERROR_PARSE
GUIDEThe full coding guide: also served verbatim as initialize's instructions and as the mmoframework://guide resource.
INDEX_LIMIT
JSONRPC_VERSION
LATEST_PROTOCOL_VERSION
PROMPTSEvery prompt this server exposes through prompts/list/prompts/get.
SEARCH_LIMIT
SEARCH_STOPWORDSThe words a relaxed search drops first, whatever their frequency: a question asks with them, documentation is not written with them, and how common they are here says nothing about that.
SEARCH_WINDOW
SECTION_CHARS
SECTION_LINES
SECTION_MATCHESHow many of the best matches come back with the whole section holding them, and how much of one is worth returning.
SUPPORTED_PROTOCOL_VERSIONS
SYMBOL_LIMIT
TOOLSEvery tool this server exposes through tools/list/tools/call.
discovered
dataListEntities($repositoryFilter)
dataListFunctions($namespace)
dataListPackages()
dataListRoutes($controllerFilter)
describeParameterDocs($fn)What each parameter's @param says: an options class (PrintOptions) explains its options there.
describeParameters($fn)
discoverDocuments()
discoverRepositories()
dispatchMethod($method, $params)
documentedPackages()
documentedSymbol($name)
documentPaths()
getPackages()The packages this server documents: every included one, then, with $discovered, every other package PackageManager read the manifest of (its most recent version), by name.
mcpInitialize($params)
mcpPromptsGet($params)
mcpPromptsList($params)
mcpResourcesList($params)
mcpResourcesRead($params)
mcpToolsCall($params)
mcpToolsList($params)
onRoute($route)
promptCreateController($args)
promptCreateEntity($args)
promptSkill($name, $skill, $args)
resourceContents($uri, $mimeType, $text)
routeMCP($route)
routeStream($route)
rpcError($id, $code, $message)
rpcResult($id, $result)
searchDocuments($documents, $query, $missing)The best matches of $query in $documents: the lines holding every word of it, each matched as a case-insensitive substring, or else the fewest consecutive lines holding them together (self::se…
searchLines($lines, $words)Where $lines hold every one of $words: each line holding them all, or else the fewest consecutive lines (at most SEARCH_WINDOW) holding them together.
searchRank($excerpt, $oneLine, $words, $phrase)What ranks a match of searchDocuments(), lowest first: its tier (0 when it holds the exact phrase, 1 when it is a single line, 2 when its words spread over several), then how many words it only fin…
searchRelaxed($documents, $query, $package)What a query matches when no line holds all of it: its most common words are dropped, one at a time, until something matches.
searchSection($lines, $at)The markdown section holding line $at: from the heading above it down to the next heading of the same level or higher, a deeper one belonging to it.
searchSections($matches, $documents)The matches, the best few carrying the whole markdown section holding them: its heading, and its lines down to the next heading of the same level or higher.
searchSymbols($query, $package)The documented symbols a query names (DocIndex::Search): those whose name holds one of its words and whose name, namespace or summary holds every one, best named first.
searchWords($query)The words of a query: white space separates them, and quotes around one are ignored -- quotes are how a search engine is asked for a phrase, and here the exact phrase ranks first anyway (self:…
skillPrompts()Every agent skill of the documented packages (.claude/skills/<skill>/SKILL.md) is a prompt too, named after its folder: a client that shows prompts as commands (`/mcp__mmoframework__mmo-release-b…
symbolDocumentation($entry, $line)Where a documentation index entry is documented, and what it says.
symbolLinks($name)
testClassFor($class)The test class conventionally matching $class: tests inserted right after its package's namespace.
textResult($text, $isError)
toolDescribeClass($args)
toolDescribeFunction($args)
toolDocumentationIndex($args)
toolGenerateControllerStub($args)
toolGenerateEntityStub($args)
toolGenerateTestStub($args)
toolGetEntitySchema($args)
toolListEntities($args)
toolListFunctions($args)
toolListPackages($args)
toolListRoutes($args)
toolSearchDocumentation($args)
toolValidateSchema($args)

Constants20

DOC_INDEX

const DOC_INDEX = 'MMO\\doc\\DocIndex'

ERROR_INTERNAL

const ERROR_INTERNAL = -32603

ERROR_INVALID_PARAMS

const ERROR_INVALID_PARAMS = -32602

ERROR_INVALID_REQUEST

const ERROR_INVALID_REQUEST = -32600

ERROR_METHOD_NOT_FOUND

const ERROR_METHOD_NOT_FOUND = -32601

ERROR_PARSE

const ERROR_PARSE = -32700

GUIDE

The full coding guide: also served verbatim as initialize's instructions and as the mmoframework://guide resource. Kept as a single source of truth on purpose.

const GUIDE = '# MMOFramework — coding guide for AI agents MMOFramework (namespace `MMO\\`, PHP 8.3+) is built from four cooperating layers: a DI/config container with lazy singletons, an entity/repository/collection/query ORM, an HTTP/CLI routing layer with attribute-driven controllers, and a plain-function event system. This is everything needed to write idiomatic code against it. For live, app-specific detail, call this server\'s tools (`describe_class`, `list_entities`, `get_entity_schema`, `list_routes`, `list_functions`, `describe_function`, `list_packages`) instead of guessing. When the application has mmo.doc, `documentation_index` lists everything documented (every namespace, class, exception, function, element and guide of every package), and `search_documentation` finds a symbol by name before searching the text. The example to copy is mmo.core\'s demo (`libs/core/demo`): an entity with validation, a JSON repository, a JSON API, an html page from a template and an event, each file explaining itself. This guide is the short version: the full one, with every trap confirmed by running code, is the `mmoframework-skill://mmo.core/mmoframework/SKILL.md` resource and its `references/*.md` (and `mmoframework-skill://mmo.client/mmo-client/...` for `.jhs` front-end files). Every skill is also a prompt of this server, named after it: bugs of a release (a recette to turn into kanban tickets, tickets to fix, a checklist to review before committing) follow `mmo-release-bugs` (`mmoframework-skill://mmo.admin/mmo-release-bugs/SKILL.md`). MMOFramework is a monorepo-of-packages: this guide covers `mmo.core` itself, but a real application usually also includes one or more `mmo-*` libraries (`mmo.client` for the front-end `.jhs` component system, `mmo.oauth`, `mmo.pdo`, `mmo.mongodb`, `mmo.pdf`, ...), each with its own namespace under `MMO\\` and its own documentation. Call `list_packages` first to see which ones this application actually has, then `resources/list`/`search_documentation` to read that package\'s own guide — do not assume mmo.core is the whole framework. A package listed with `included: false` is only discovered: its documentation is served, but its code is not loaded (`describe_class` cannot see its classes) until the application depends on it. ## 1. Singletons — never `new` them Any class using the `tSingleton` trait (`Application`, `PackageManager`, `Cron`, `I18n`, and every controller) is a per-request singleton — so is `Configuration`, which has a `Shared()` of its own: ```php $app = \\MMO\\core\\Application::Shared(); // NOT `new Application()` MyController::Shared()->doThing(); ``` `Shared()` lazily builds the instance the first time it is called, from whatever was loaded into `Configuration` for that class name. `ClassName::Unshare()` discards the cached instance, first keeping its state as the class\'s config (the next `Shared()` picks up where it left off); `Unshare(false)` keeps nothing. ## 2. Configuration / dependency injection `Configuration::Shared()` is one nested `class-or-path => settings` array, populated via `\\MMO\\config([...])` and read via `\\MMO\\get(\'Some/Path\')`. A value shaped `[\'$\' => SomeClass, ...args]` is a lazy object descriptor, unserialized into a real instance the first time it is read: ```php \\MMO\\config([ MyController::class => [\'repositories\' => [\'main\' => [\'$\' => \'#repo\']]], \'repo\' => [\'$\' => \\MMO\\core\\entities\\JsonRepository::class, \'filenames\' => [\'items\' => __DIR__ . \'/data/items.json\'], \'collections\' => [\'items\' => MyEntity::class]], ]); ``` `#[Serialize(unserialize: \'setFoo\')]` on a property tells the container to call `setFoo($value)` instead of a plain assignment when an instance is built from config. ## 3. Entities — the ORM layer An **Entity** is one record, a **Collection** holds every loaded entity of one class, a **Repository** owns one or more collections and persists them. ```php use MMO\\core\\entities\\Entity; use MMO\\core\\attributes\\ArrayProperty; /** @unique name */ class Article extends Entity { #[ArrayProperty()] protected string $name = \'\'; #[ArrayProperty()] protected int $views = 0; #[ArrayProperty(schema: \'[string]\')] protected array $tags = []; } ``` Rules that matter: - Do not redeclare `id`, `label`, `title`, `status`, `ctime`, `mtime` — `Entity` already declares them with fixed types; redeclaring with an incompatible type is a fatal error. - `#[ArrayProperty()]` with no arguments on a plain `protected` property is normal and correct: the name, getter, setter and schema type are all inferred from the property itself. Only pass `getter:`/`setter:`/ `schema:` to override that inference, or put the attribute on the *class* (with an explicit `getter:` and no backing property) for a computed/derived field. - There is no magic `__get`/`__set`. Always read/write through `$entity->get(\'name\')`, `$entity->set(\'name\', $value)`, `$entity->push(\'tags\', $value)`. - `@unique name` (class docblock) tells `Collection::getOrCreateEntity()` which field(s) identify "the same" entity; without it, `getOrCreateEntity()` always creates a new one. A compound key is **comma**-separated on one line (`@unique firstName,lastName`); repeat the `@unique` tag on its own line for multiple, independent alternative keys. - Nothing is persisted until you call `\\MMO\\commit($origin)` — entities track their own dirty state (`isChanged()`, `isCreated()`, `isRemoved()`, `isValid()`/`getErrors()`). - An entity checks values as they are set **in dev mode only**: check user input with `Person::CheckData($input, $errors)` before creating an entity (or `CheckData([...$person->toArray(), ...$input])` before changing one): nothing is touched when it is refused. `@required` only means the key is present; use `@minLength 1` for "not empty". - Status lifecycle: `cStatus::{DRAFT,PENDING,ACTIVE,VALIDATED,FINAL,ARCHIVE}`, changed via `$entity->setStatus(...)`; `archive()` (soft) then `removePermanently()` (hard, archived entities only) are the two deletion steps. ```php $repo = new \\MMO\\core\\entities\\JsonRepository([\'articles\' => \'/path/articles.json\'], [\'articles\' => Article::class]); $col = $repo->getCollection(\'articles\'); $a = $col->getOrCreateEntity([\'name\' => \'Hello\']); $a->set(\'views\', $a->get(\'views\') + 1); \\MMO\\commit($someOrigin); ``` Query a collection with `$collection->query($filter, fields: [], sort: [\'-createdAt\'], limit: 10, offset: 0) ->getEntities()`: `getEntities()` runs it (through the repository, if one is set), while `dispatch()` only notifies the query\'s listeners. Filters are `$`-prefixed (`$and`, `$or`, `$in`, `$gt`, `$gte`, `$lt`, `$lte`, `$regex`), `\'path/to/field\' => value` for equality. ## 4. Controllers & routing A controller is a singleton whose public methods, tagged `#[APIRoute(...)]`, become HTTP routes. ```php use MMO\\core\\classes\\aDataController; use MMO\\core\\attributes\\APIRoute; use MMO\\core\\server\\Route; class WidgetController extends aDataController { #[APIRoute(\'GET|widgets/{id}.json\', \'Get one widget\', mediaTypes: [\'json\'])] public function routeGet(Route $route): array { return [\'id\' => $route->get(\'id\')]; } } ``` - Extend `aDataController` when every route just returns a plain array — its `onRoute()` already sets `Response::Json()` for you. - Extend plain `aController` only if you return an `iResponse` yourself, or override `onRoute()` — a bare `aController` subclass returning an array crashes with "route has no response set." - `$route->get($key)` checks, in this fixed order: attributes (path placeholders) → query → body → headers → cookies → files. - Wire routes with `\\MMO\\config([\\MMO\\core\\Application::class => [\'routes\' => [WidgetController::class]]])` or `\\MMO\\app()->connectRoutes(WidgetController::class)`. - `order:` on `#[APIRoute]` orders the routes of the controller that handles the request (lower runs first, as for middlewares: every matching one runs). It does not choose between controllers: the one built first (normally the first listed in `routes`) claims the request in its `onRoute()`, whatever its routes\' `order` — list a catch-all controller last. ## 5. Schemas `Schema::FromClass(SomeEntity::class)` derives a JSON-Schema description from `#[ArrayProperty]` attributes **plus each property\'s own PHP docblock** — a property\'s doc comment doubles as its schema, so you rarely need `#[ArrayProperty(schema: ...)]` at all: ```php fragment /** * the user\'s age * @minimum 0 * @maximum 150 * @required */ #[ArrayProperty()] protected int $age = 0; /** @format email */ #[ArrayProperty()] protected string $email = \'\'; ``` Recognized property-docblock tags: a bare description line, `@type`/`@var` (overrides the inferred type — see below), `@label`, `@title`, `@description`, `@order`, `@icon`, `@view`, `@store`, `@hidden`, `@noindex`, `@required` (bare, `true`/`false`, or a comma-separated list of required property names on the *class*), and any JSON-Schema primitive constraint as a tag: `@minLength`, `@maxLength`, `@pattern`, `@format`, `@multipleOf`, `@minimum`, `@exclusiveMinimum`, `@maximum`, `@exclusiveMaximum`, `@minProperties`, `@maxProperties`, `@minItems`, `@maxItems`, `@uniqueItems`, `@minContains`, `@maxContains`. `@var` types (a `schema:` string takes the same): `integer|number|string|boolean|null` (or `int`/`float`/`bool`), `?T` (nullable), `"a"|"b"` (an enum), a class (`\\Full\\Name`, or a name of the class\'s own namespace: `use` is not read), `[T]` (a list), `{key: T, ...: T}` (an object, `{T}` a map) -- and phpstan\'s arrays: `list<T>`, `array<T>`, `T[]`, `array<int, T>` are `[T]`, `array<string, T>` is `{T}`, `array<mixed>` a list of anything. In a docblock, what the framework cannot check (`non-empty-string`, an imported class, `array{id: int}`) is left unchecked. There is no `$ref`, `enum[...]`, `not()`/`anyOf()`/`oneOf()`/`allOf()`. `Schema::Defs(...$classes)` builds a `$defs` block for several classes at once (this is what `RepositoryController::routeSchemas()` returns). `Validator`/`Coercer` (used internally by the entity layer and by `#[APIRoute]`\'s body/query declarations) check and convert values against a schema. Do not invent a `@hash`/`@uid`/`@internal` tag — despite appearing in some documentation drafts, they are not implemented; `uid()` is computed from the collection\'s own uid plus the entity\'s `id`, and there is no way to exclude a property from the schema other than `@hidden`/`@noindex` (which hide it from a UI/index, not from the schema itself) or not tagging it `#[ArrayProperty]` at all. ## 6. Events Every cross-cutting extension point is a class extending `Event`: ```php class UserCreated extends Event { public function __construct(object|string $origin, public readonly string $userId) { parent::__construct($origin); } } UserCreated::AddListener(fn(UserCreated $e) => null /* ... */, order: 0); UserCreated::Send($origin, $userId); ``` An event whose listeners answer extends `Signal` (listeners call `setReturn()`, the sender reads `getReturn()`); a listener is usually a module method tagged `#[Listen(UserCreated::class)]`, connected when the module is first built (`Module::Shared()`). Extension points you will actually touch: `Create` (`\\MMO\\create()`), `Fetch` (`\\MMO\\fetch()`), `Commit` (`\\MMO\\commit()`), `Log` (`\\MMO\\info()`/`error()`/`warning()`/...), `DocumentInit`/`DocumentRender`. ## 7. Namespaced plain functions Utility code is *not* static-class methods — it is plain functions grouped by domain and called fully qualified: `\\MMO\\core\\arrays\\get($src, $path)`, `\\MMO\\core\\strings\\jsonEncode($data)`, `\\MMO\\core\\paths\\basename($file)`, `\\MMO\\core\\files\\scanFolder($dir)`, etc. Use the `list_functions`/ `describe_function` tools to see exactly which helpers exist in a given namespace and their real signature — do not assume one exists without checking. ## 8. Running shell/SSH commands `MMO\\core\\server\\ShellRunner` wraps either the local `Shell` or a remote `SSH` connection (anything implementing `iShell`) behind one API returning a `ShellResult` (`->out`, `->err`, `->code`, `->isOk()`, `->getLines()`, `->assert($origin)`) with a real exit code either way — prefer it over calling `$shell->exec()`/`execSafe()` directly, whose failure reporting differs between the local and SSH implementations. It also has dry-run support and the handful of filesystem primitives (`exists`, `read`, `write`, `symlink`, ...) a deployment script needs to run identically locally or over SSH. ## 9. Sharp edges - `??` binds looser than `===`/`&&`/`||` — always parenthesize: `$a === ($b ?? $c)`, never `$a === $b ?? $c` (the latter\'s `?? $c` is permanently dead code, since a `===` result is never `null`). - `$document->import($filename, $vars)` does a **literal** `file_exists()`/`require()` lookup against every registered `addImportPath()` folder — no extension is added or assumed, so pass the real on-disk filename verbatim, extension included if the file has one (`import(\'welcome.phtml\', $vars)` for a file literally named `welcome.phtml`). It also does **not** `extract()` `$vars` into local variables despite what an older docblock claimed — read them inside the template as `$_VARS[\'key\']`, never a bare `$key`. - `HtmlPrinter` prints a string as markup: user input printed into an `HtmlDocument` is an XSS unless escaped. In a template, `htmlspecialchars()` what you print; through the printer, pass `new PrintOptions(escape: true)` (`$this->renderValue($value, new PrintOptions(escape: true))`); in a tree of nodes, hold it in an `XmlText` (`new XmlNode(\'td\', [], [new XmlText($name)])`); or give the whole document `new DocumentOptions(print: new PrintOptions(escape: true))`, which leaves what a `<script>` or a `<style>` holds as it is. `setTitle()` takes text and escapes it: do not escape a title first. Nor what you log: a log\'s message and backtrace are always escaped, so log an untrusted value as it is. - Printer and document options: pass `PrintOptions`/`DocumentOptions` (`MMO\\core\\printers`, `MMO\\core\\documents`) rather than an array — a misspelled array key (`[\'escpae\' => true]`) is silently ignored, a misspelled named argument is an error. - Several operators on one filter field must all match (`[\'age\' => [\'$gte\' => 18, \'$lt\' => 65]]`); an unknown operator is an error naming the right one. `Entity::CheckData()` rejects a key the entity does not have. - `\\MMO\\core\\shell\\escape()` is **not** injection-safe: it single-quotes an argument (`\'\\\'\'` for a quote inside), but returns one that already starts and ends with `\'` as it is, taken for already quoted — `escape("\'a\'; touch pwned; echo \'b\'")` comes back unchanged. Quote a value you do not control with PHP\'s `escapeshellarg()`, or a `ShellRunner`\'s `quote()` (for its target shell, local or SSH). `Process` takes one command string, not an argument list: build it from fixed pieces and quote each value you interpolate. On Windows, `escape()` only wraps in double quotes, which has caveats of its own (`%VAR%` expansion). - Middleware/job `order`/`priority` runs ascending (lower first) everywhere in this framework. ## 10. Verifying your work ```bash php -l path/to/File.php ``` If unsure how something behaves, write a five-line script (`require \'bootstrap.php\';` then exercise the class directly) and run it — most of this framework needs no server or database to test in isolation (`JsonRepository` persists to a plain JSON file).'

INDEX_LIMIT

const INDEX_LIMIT = 500

JSONRPC_VERSION

const JSONRPC_VERSION = '2.0'

LATEST_PROTOCOL_VERSION

const LATEST_PROTOCOL_VERSION = '2025-06-18'

PROMPTS

Every prompt this server exposes through prompts/list/prompts/get.

protected const PROMPTS = […3 items]

SEARCH_LIMIT

const SEARCH_LIMIT = 40

SEARCH_STOPWORDS

The words a relaxed search drops first, whatever their frequency: a question asks with them, documentation is not written with them, and how common they are here says nothing about that. new is in half the documentation because php spells an instance that way, and how in a quarter of it -- dropping how first is what a reader means. self::searchRelaxed. They are never dropped from a search that matches whole.

const SEARCH_STOPWORDS = […39 items]

SEARCH_WINDOW

const SEARCH_WINDOW = 3

SECTION_CHARS

const SECTION_CHARS = 4000

SECTION_LINES

const SECTION_LINES = 60

SECTION_MATCHES

How many of the best matches come back with the whole section holding them, and how much of one is worth returning. An agent answers from what a search gives it: over an evaluation run it called search_documentation 75 times and read a page in full twice, so an excerpt that only looks like an answer becomes a wrong answer.

const SECTION_MATCHES = 3A section of this documentation is 14 lines in the middle, 44 at the ninth decile: 60 returns most whole.

SUPPORTED_PROTOCOL_VERSIONS

const SUPPORTED_PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05']

SYMBOL_LIMIT

const SYMBOL_LIMIT = 15

TOOLS

Every tool this server exposes through tools/list/tools/call.

protected const TOOLS = […13 items]`handler` names a protected method of this class taking (array $arguments): array. `properties` is optional: a tool taking no argument leaves it out ([`self::mcpToolsList()`](https://mcp.mmoframework.fr/documentation/mmo.core/php/controllers/MCPController.md#mcpcontroller-mcptoolslist)).

Properties1

discovered

bool $discovered = false
Type
bool

Methods55

dataListPackages()

MCPController.php:1552
protected function dataListPackages(): array
returnsarray[name, title, version, included, namespace, description, status, keywords, dependencies, documentation, links] the PackageManifest showcase metadata of every package this server documents (getPackages).

discoverDocuments()

MCPController.php:1620
protected function discoverDocuments(): array
returnsarray[uri, path, package, included, title] every *.md doc file of every documented package's (getPackages) documentations folders and agent skills.

discoverRepositories()

MCPController.php:1495
protected function discoverRepositories(): array
returnsarray[iRepository] every repository exposed by a connected controller's getRepositories().

documentedSymbol($name)

MCPController.php:1240
protected function documentedSymbol(string $name): ?array
$namestringa fully-qualified name, as the documentation index keys it
returnsnull | arraymixed>|null mmo.doc's index entry for it (DocIndex::Load), when mmo.doc is loaded, it is documented, and its package is one this server documents

getPackages()

MCPController.php:1602

The packages this server documents: every included one, then, with $discovered, every other package PackageManager read the manifest of (its most recent version), by name. Included ones come first: a search ranks equal matches in this order.

protected function getPackages(): array
returnsarray
$this->getPackages(); // [mmo.core, mmo.client, ...], then [mmo.admin, mmo.pdo, ...] when $discovered

promptSkill($name, $skill, $args)

MCPController.php:1435
protected function promptSkill(string $name, array $skill, array $args): string
$namestring
$skillarray
$argsarrayrequest: what to do with it
returnsstringthe skill, its frontmatter left out, then where its references are and the request

routeMCP($route)

MCPController.php:509
#[MMO\core\attributes\APIRoute('POST|mcp.json', 'Model Context Protocol JSON-RPC 2.0 endpoint for the MMOFramework MCP server.', mediaTypes: ['json'])] function routeMCP(MMO\core\server\Route $route): array
$routeRoute
returnsarray

routeStream($route)

MCPController.php:544
#[MMO\core\attributes\APIRoute('GET|mcp.json', 'The MCP endpoint answers POST only: no SSE stream is opened (405).', mediaTypes: ['json'])] function routeStream(MMO\core\server\Route $route): array
$routeRoute
returnsarray

searchDocuments($documents, $query, $missing)

MCPController.php:1035

The best matches of $query in $documents: the lines holding every word of it, each matched as a case-insensitive substring, or else the fewest consecutive lines holding them together (self::searchLines()).

protected function searchDocuments(array $documents, string $query, ?array &$missing = null): arrayRanked by self::searchRank(), the order of $documents breaking ties.
$documentsarray
$querystringwords separated by white space; quotes around a word are ignored
$missing = nullnull | arrayreceives the words no document holds at all: why nothing matched
returnsarraystring, line: int, excerpt: string}> at most SEARCH_LIMIT. A match over several lines is reported at its first, its excerpt joins them.
$this->searchDocuments(['doc://a' => "An entity is stored\nin a repository."], 'entity repository'); // [['resource' => 'doc://a', 'line' => 1, 'excerpt' => 'An entity is stored in a repository.']]

searchLines($lines, $words)

MCPController.php:1070

Where $lines hold every one of $words: each line holding them all, or else the fewest consecutive lines (at most SEARCH_WINDOW) holding them together. Such windows never overlap, nor include a line that matches on its own.

protected function searchLines(array $lines, array $words): array
$linesarray
$wordsarray
returnsarrayint}> the index of the first and of the last line of each match, in the order of $lines
$this->searchLines(['an entity', 'in a repository', 'an entity repository'], ['entity', 'repository']); // [[0, 1], [2, 2]]

searchRank($excerpt, $oneLine, $words, $phrase)

MCPController.php:1107

What ranks a match of searchDocuments(), lowest first: its tier (0 when it holds the exact phrase, 1 when it is a single line, 2 when its words spread over several), then how many words it only finds inside another word (entity in identity, unlike Repository in JsonRepository), then the length of the shortest stretch of it holding every word. The phrase only counts where a word starts too: get entity is not in target entity.

protected function searchRank(string $excerpt, bool $oneLine, array $words, string $phrase): array
$excerptstringthe text matched
$oneLineboolwhether it is a single line
$wordsarray
$phrasestringthe words, as the query gave them
returnsarrayint, int}
$this->searchRank('an entity in a repository', true, ['entity', 'repository'], 'entity repository'); // [1, 0, 22]

searchRelaxed($documents, $query, $package)

MCPController.php:1000

What a query matches when no line holds all of it: its most common words are dropped, one at a time, until something matches. A wrong guess at a name then still finds the page holding the rest of it, rather than nothing at all -- one word too many in a guessed command used to hide the page that names the real one.

protected function searchRelaxed(array $documents, string $query, string $package = ''): array(No example of such a query here on purpose: this page is searched too, and it would answer for itself.) A word no documentation holds goes first -- it alone makes the whole query match nothing -- then a question word ([`self::SEARCH_STOPWORDS`](https://mcp.mmoframework.fr/documentation/mmo.core/php/controllers/MCPController.md#mcpcontroller-search-stopwords)), then the most common, because it says the least about what is looked for: `make` is in a seventh of the documentation, `project` in a fourteenth. A tie goes to the shorter word. The last word left is never dropped -- one word alone matches too much to mean anything.
$documentsarray
$querystring
$package = ''stringthe package searchSymbols is restricted to, '' for all
returnsarraylist<array{resource: ?string, line: int, excerpt: string}>, list<array{resource: string, line: int, excerpt: string}>} the words dropped, then the symbols and the matches the rest found -- all three empty when even one word finds nothing.

searchSection($lines, $at)

MCPController.php:954

The markdown section holding line $at: from the heading above it down to the next heading of the same level or higher, a deeper one belonging to it. Without a heading above, the section starts at the first line and any heading ends it.

protected function searchSection(array $lines, int $at): array
$linesarray
$atintthe index of the line the section must hold
returnsarrayint, string} the first and last line of the section, and the text of its heading ('' when none)
$this->searchSection(['# A', 'one', '## B', 'two', '# C'], 3); // [2, 3, 'B']

searchSections($matches, $documents)

MCPController.php:912

The matches, the best few carrying the whole markdown section holding them: its heading, and its lines down to the next heading of the same level or higher. A search excerpt is three lines at most, which is enough to look like an answer and not enough to be one -- and the follow-up read that would settle it is the call an agent does not make. Only the first SECTION_MATCHES distinct sections are given; the rest stay excerpts.

protected function searchSections(array $matches, array $documents): array
$matchesarray
$documentsarray
returnsarraythe same matches, the best few with a heading and a section more

searchSymbols($query, $package)

MCPController.php:1135

The documented symbols a query names (DocIndex::Search): those whose name holds one of its words and whose name, namespace or summary holds every one, best named first. Nothing when mmo.doc is not loaded.

protected function searchSymbols(string $query, string $package = ''): array
$querystring
$package = ''stringonly this package's, or every documented package's when ''
returnsarray?string, line: int, excerpt: string, symbol: string, kind: string, language: string, package: ?string, url: ?string}> at most SYMBOL_LIMIT
$this->searchSymbols('Route stop', 'mmo.core')[0]['symbol']; // 'MMO\core\server\Route::stop'

searchWords($query)

MCPController.php:978

The words of a query: white space separates them, and quotes around one are ignored -- quotes are how a search engine is asked for a phrase, and here the exact phrase ranks first anyway (self::searchRank).

protected function searchWords(string $query): array
$querystring
returnsarray
$this->searchWords('"entity" repository'); // ['entity', 'repository']

skillPrompts()

MCPController.php:1416

Every agent skill of the documented packages (.claude/skills/<skill>/SKILL.md) is a prompt too, named after its folder: a client that shows prompts as commands (/mcp__mmoframework__mmo-release-bugs) runs the skill without knowing its resource. Its description is the skill's own.

protected function skillPrompts(): array
returnsarrayarray{description: string, uri: string, path: string, references: list<string>}> by name

symbolDocumentation($entry, $line)

MCPController.php:1262

Where a documentation index entry is documented, and what it says.

protected function symbolDocumentation(array $entry, bool $line = true): array
$entryarray
$line = trueboolalso find the line of its generated page it starts at (reads the page)
returnsarraystring, url?: string, resource?: string, line?: int} url is its html page, resource its generated markdown page (the mmoframework-docs:// uri resources/read serves)

testClassFor($class)

MCPController.php:811

The test class conventionally matching $class: tests inserted right after its package's namespace.

protected function testClassFor(string $class): string
$classstring
returnsstring
$this->testClassFor('MMO\core\entities\Person'); // 'MMO\core\tests\entities\TestPerson'
Inherited members (55)
ConstantsDeclared byDescription
ALERTiLogLevelAlert: Action must be taken immediately.
ALLiLogLevelMask for all
ALL_PLUSiLogLeveldebug everything
BACKTRACEiLogLevel
CRITICALiLogLevelCritical: Critical conditions.
DEBUGiLogLevelDebug: Detailed debug information.
DEEPiLogLeveldebug everything with arguments
EMERGENCYiLogLevelEmergency: System is unusable.
ERRORiLogLevelError: Runtime errors that do not require immediate action but should typically be logged and monitored.
ERROR_PLUSiLogLevelMask for all error
FATALiLogLevelMask for all fatal error
INFOiLogLevelInfo: Interesting events.
INFO_PLUSiLogLevelMask for all errors/warning/notices/info
MEMORYiLogLevelinclude backtrace object
NOTICEiLogLevelNotice: Normal but significant events.
NOTICE_PLUSiLogLevelMask for all errors/warning/notices/info
TRACEiLogLevelinclude backtrace as origins
WARNINGiLogLevelWarning: Exceptional occurrences that are not errors.
WARNING_PLUSiLogLevelMask for all errors and warnings
PropertiesDeclared byDescription
verbosityaModule
MethodsDeclared byDescription
alert($message, $context)aModuleAction must be taken immediately.
backtraces($message, $context)aModuleForce-log a message at INFO level together with a full stack trace/memory snapshot (iLogLevel::BACKTRACE), bypassing the verbosity mask entirely (any non-zero verbosity is enough to trigger it).
Configure($config)aModuleMerge $config into this module's configuration in the shared Configuration.
critical($message, $context)aModuleCritical conditions. No-op if CRITICAL is currently filtered out by the verbosity mask.
debug($message, $context)aModuleDetailed debug information.
debugName()aModuleHuman-readable identifier used as the default {origin} in this instance's log messages.
DefaultConfig()aModuleThe config a module starts from before anything loaded via Configuration is merged in.
emergency($message, $context)aModuleSystem is unusable. No-op if EMERGENCY is currently filtered out by the verbosity mask.
error($message, $context)aModuleRuntime errors that do not require immediate action but should typically be logged and monitored.
ExtractRoutes($config)aControllerExtract all the APIRoute from this class, regardless of the fact they are registered in the application
getVerbosity()aModule
HasRoutes()aControllerTells if any APIRoute are registered in the application
info($message, $context)aModuleInteresting events. No-op if INFO is currently filtered out by the verbosity mask.
Instantiate($config)aModule(Re)create the shared instance immediately, configured with $config merged into the class's existing config.
IsShared()aModule
Link($method, $data)aControllerBuild a Link (URL + label/icon/description/title, taken from the route's APIRoute metadata) for a registered route.
log($level, $message, $context)aModuleLogs with an arbitrary level.
notice($message, $context)aModuleNormal but significant events.
offsetExists($key)aModule\ArrayAccess::offsetExists -- tells whether $key is a declared property at all, regardless of whether it is currently readable/writable through array access.
offsetGet($key)aModule\ArrayAccess::offsetGet -- reads a property directly, unless it has a #[Serialize(serialize: 'methodName')] attribute, in which case that zero-argument method is called instead (e.g.
offsetSet($key, $value)aModule\ArrayAccess::offsetSet -- sets a property directly, unless it has a #[Serialize(unserialize: 'methodName')] attribute, in which case that method is called with $value instead (e.g.
offsetUnset($key)aModule\ArrayAccess::offsetUnset -- equivalent to $module[$key] = null.
Route($method)aControllerGet a registered APIRoute WARNING: if this class was not registered in the application, it will return null.
Routes()aControllerGet a list of registered APIRoutes (from this class) in the application WARNING: if this class was not registered in the application, it will return an empty array.
serialize($path, $serializer)aModule\MMO\core\interfaces\iSerializable::serialize -- exports every property (via
setVerbosity($verb)aModuleSet the current verbosity mask, replacing whatever it was before.
Shared()aModuleGet the shared instance, creating (and configuring) it on first call.
testVerbosity($level)aModuleTest whether a given level (or set of levels) is currently enabled by this instance's verbosity mask.
traitUnshare($serialize)aModuleRemove the shared instance, so the next Shared() call creates a fresh one.
unserialize($data, $path, $instances)aModule\MMO\core\interfaces\iSerializable::unserialize -- merges $data onto DefaultConfig, runs it through the (possibly overridden) constructor, then applies each resulting value to its property (via
Unshare($serialize)aModuleLike tSingleton::Unshare, but also disconnects this module's instance from every event it was listening to (via Event::DisconnectListeners) before dropping it.
Url($method, $data)aControllerBuild the URL for a registered route, filling in any {placeholder} from $data.
warning($message, $context)aModuleExceptional occurrences that are not errors.
__construct($config)aModuleprotected constructor, so it is not possible to do new Module(...) called by the serialization process just before being injected with data.
__wakeup()aControllerCalled automatically right after the controller's shared instance is built (aModule::unserialize): registers onRoute as a Route listener with a low priority (-10), so it…