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.jsondeclares the scopes the extension needs.bootstrap.phpcan 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:
| Gate | Required value |
|---|---|
Manifest requiredScopes | content.read |
| Operator extension grant | content.read |
Action metadata requiredScope | MyronApiScopes::CONTENT_READ |
| API or MCP caller held scope | content.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.phpapp/extensions/my_vendor/bootstrap.phpapp/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:
- Send
tools/list. - Confirm
x_my_vendor_status_status_getis present. - Send
tools/callwith an emptyargumentsobject.
Failure Modes
| Failure | Likely cause | Safe fix |
|---|---|---|
| Action does not load | Extension is unregistered, disabled, under-granted, or has a bootstrap error. | Check Installed Extensions and load errors. |
| Registry rejects action | URI/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_required | Request has no authenticated session or bearer token. | Use an authenticated caller. |
API returns scope_insufficient | Caller lacks content.read. | Use a caller with the action scope; do not broaden scopes automatically. |
Tool missing from tools/list | Action is not loaded, exposeToMcp is false, or caller lacks scope. | Verify load state, metadata, and caller scope. |
| Tool call returns an error result | The dispatcher reached the action and the action reported a declared failure. | Read the action error and fix the stated precondition or input. |