MyronCMS Developers Hub Docs sandbox review

Build Extensions

Write A Read Action

A read action is a MyronAction whose metadata declares a GET method and whose execute() method returns data without changing state. For extensions, the action lives in local files under app/extensions/{vendor}/, then loads only after the operator registers, enables, and grants the extension.

Use this pattern for list, fetch, status, lookup, and small introspection endpoints that should be callable through the direct API and optionally exposed as MCP tools.

Goal

Create a scoped read action with:

  • URI: /api/v1/x/my_vendor/status
  • domain: x.my_vendor.status
  • method: GET
  • required scope: content.read
  • MCP exposure: exposeToMcp: true

Prerequisites

  • The extension package exists under app/extensions/my_vendor/.
  • manifest.json declares the scopes the extension needs.
  • bootstrap.php can require the action file and return the action instance.
  • The operator can register and enable the extension with content.read.

Human Path

Create an action class such as:

<?php

declare(strict_types=1);

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

    public function metadata(): MyronActionMetadata
    {
        return new MyronActionMetadata(
            uri: '/api/v1/x/my_vendor/status',
            method: 'GET',
            domain: 'x.my_vendor.status',
            resource: 'status',
            verb: 'get',
            inputSchema: ['type' => 'object', 'properties' => []],
            outputSchema: [
                'type' => 'object',
                'properties' => [
                    'siteType' => ['type' => 'string'],
                    'ready' => ['type' => 'boolean'],
                ],
            ],
            requiredScope: MyronApiScopes::CONTENT_READ,
            summary: 'Return this extension status for the current install.',
            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([
            'siteType' => $this->sdk->getSiteType(),
            'ready' => true,
        ]);
    }
}

Register it from bootstrap.php:

<?php

declare(strict_types=1);

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

return [
    new MyVendorStatusAction($sdk),
];

AI Path

An AI coding agent can author the PHP files in the repository and run local verification. That is local authoring.

An installed MCP client cannot author the extension files. It can discover and call the action only after the extension has loaded and the caller holds the required scope. For the example above, the default MCP tool name is:

x_my_vendor_status_status_get

That name is derived from {domain}_{resource}_{verb} and normalized to strict-client-safe snake_case.

Scopes

The extension grant and the runtime caller scope are separate gates:

GateRequired value
Manifest requiredScopescontent.read
Operator extension grantcontent.read
Action metadata requiredScopeMyronApiScopes::CONTENT_READ
API or MCP caller held scopecontent.read

If the extension is under-granted, the loader should skip or fail the extension. If the caller is under-scoped, direct API dispatch returns scope_insufficient, and MCP tools/list omits the tool for that caller.

Artifacts

Local files:

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

Runtime surfaces:

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

Safety Boundary

Keep reads read-only. Do not use a GET action to write rows, update settings, send mail, or trigger side effects. Declare sideEffects: [] only when the implementation actually has no side effects.

Keep the extension namespace intact. ActionRegistry::registerExtension() rejects extension actions whose URI does not start with /api/v1/x/{vendor}/, whose domain does not start with x.{vendor}., or whose schemas are empty arrays.

Verification

Run the foundation check:

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

After the extension is registered, enabled, and granted, call the direct API:

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

Then verify MCP discovery with the same authorization context:

  1. Send tools/list.
  2. Confirm x_my_vendor_status_status_get is present.
  3. Send tools/call with an empty arguments object.

Failure Modes

FailureLikely causeSafe fix
Action does not loadExtension is unregistered, disabled, under-granted, or has a bootstrap error.Check Installed Extensions and load errors.
Registry rejects actionURI/domain namespace is wrong or a schema is empty.Keep URI under /api/v1/x/{vendor}/, domain under x.{vendor}., and use object schemas.
API returns auth_requiredRequest has no authenticated session or bearer token.Use an authenticated caller.
API returns scope_insufficientCaller lacks content.read.Use a caller with the action scope; do not broaden scopes automatically.
Tool missing from tools/listAction is not loaded, exposeToMcp is false, or caller lacks scope.Verify load state, metadata, and caller scope.
Tool call returns an error resultThe dispatcher reached the action and the action reported a declared failure.Read the action error and fix the stated precondition or input.