MyronCMS Developers Hub Docs sandbox review

Start Here

Hello World Extension

This tutorial builds the smallest useful extension: a local package that returns a greeting from a read-only API action. The working reference is app/extensions/myron-sample/.

Goal

Create a local extension that registers:

  • vendor: my_vendor
  • action URI: /api/v1/x/my_vendor/hello
  • action domain: x.my_vendor.hello
  • method: GET
  • scope: content.read
  • MCP exposure: exposeToMcp: true

Prerequisites

  • You are working in the MyronCMS repository.
  • You can create files under app/extensions/.
  • The install has extension scope grant tables available.
  • The operator can register and enable the extension with content.read.

Human Path

Create the extension directory:

app/extensions/my_vendor/
  manifest.json
  bootstrap.php
  actions/MyVendorHelloAction.php

Create manifest.json:

{
  "vendor": "my_vendor",
  "name": "My Vendor Hello",
  "version": "0.1.0",
  "requiredScopes": ["content.read"],
  "hasMigrations": false,
  "adminRoutes": []
}

Create bootstrap.php:

<?php

declare(strict_types=1);

require_once __DIR__ . '/actions/MyVendorHelloAction.php';

return [
    new MyVendorHelloAction($sdk),
];

Create actions/MyVendorHelloAction.php:

<?php

declare(strict_types=1);

final class MyVendorHelloAction implements MyronAction
{
    public function __construct(private readonly MyronExtensionSDK $sdk)
    {
    }

    public function metadata(): MyronActionMetadata
    {
        return new MyronActionMetadata(
            uri: '/api/v1/x/my_vendor/hello',
            method: 'GET',
            domain: 'x.my_vendor.hello',
            resource: 'hello',
            verb: 'get',
            inputSchema: ['type' => 'object', 'properties' => []],
            outputSchema: [
                'type' => 'object',
                'properties' => [
                    'message' => ['type' => 'string'],
                    'siteType' => ['type' => 'string'],
                ],
            ],
            requiredScope: MyronApiScopes::CONTENT_READ,
            summary: 'Return a hello-world extension greeting.',
            sideEffects: [],
            preconditions: ['The my_vendor extension is enabled and granted content.read.'],
            postconditions: [],
            audienceClass: 3,
            exposeToMcp: true,
        );
    }

    public function execute(array $input, MyronActionCallerContext $caller): MyronActionResult
    {
        return MyronActionResult::ok([
            'message' => 'Hello from my_vendor extension',
            'siteType' => $this->sdk->getSiteType(),
        ]);
    }
}

Register and enable the extension from the Extensions admin surface, granting content.read. Until that happens, the loader skips the extension when grants are required.

AI Path

An AI coding agent can perform the same local file-authoring steps in the repository. It must not claim the running MCP client created the PHP files.

After the files exist and the operator registers/enables/grants the extension, an installed API or MCP client can verify the result:

  • API path: GET /api/v1/x/my_vendor/hello
  • MCP discovery: tools/list
  • MCP call: tools/call with tool name x_my_vendor_hello_hello_get

The MCP tool name comes from the default tool-name mapping: {domain}_{resource}_{verb}, normalized for strict clients.

Scopes

The manifest declares content.read, and the action metadata requires MyronApiScopes::CONTENT_READ. Both gates matter:

  • the extension grant must include content.read,
  • the API or MCP caller must also hold content.read.

Artifacts

The local artifacts are:

  • app/extensions/my_vendor/manifest.json
  • app/extensions/my_vendor/bootstrap.php
  • app/extensions/my_vendor/actions/MyVendorHelloAction.php

The runtime entrypoints are:

  • GET /api/v1/x/my_vendor/hello
  • MCP tools/list
  • MCP tools/call

Safety Boundary

Keep the extension namespaced. ActionRegistry::registerExtension() rejects an action if the URI does not start with /api/v1/x/my_vendor/, if the domain does not start with x.my_vendor., or if either schema is an empty array.

Do not use this tutorial to mutate core tables, add public render hooks, inject core admin rail sections, or bypass extension scope grants.

Verification

Run the local foundation check:

php app/tests/verify-extension-system-foundation.php

Then verify the action with the API using a bearer token that holds content.read:

curl -sS \
  -H "Authorization: Bearer ${MYRON_TOKEN}" \
  "https://example.test/api/v1/x/my_vendor/hello"

A successful response uses the canonical action envelope and contains a result with the greeting message.

For MCP, call tools/list with the same authorization context and confirm x_my_vendor_hello_hello_get appears. Then call that tool with an empty argument object.

Failure Modes

FailureLikely surface
Vendor mismatchExtensionManifest::fromFile() rejects the manifest.
Unknown scopeManifest scope normalization rejects the manifest.
Extension not registeredLoader skips the extension.
Extension disabledLoader does not register its actions.
Missing grantLoader reports an ungranted required scope.
Wrong URI prefixActionRegistry::registerExtension() rejects the action.
Wrong domain prefixActionRegistry::registerExtension() rejects the action.
Empty schemaActionRegistry::registerExtension() rejects the action.
Missing bearer tokenAPI dispatch returns auth_required for private actions.
Insufficient caller scopeAPI dispatch returns scope_insufficient; MCP omits or rejects the tool depending on the call path.
exposeToMcp: falseAPI may still exist, but MCP tools/list will not show the action.