MyronCMS Developers Hub Docs sandbox review

Integrations

Build A Provider Transport

This tutorial shows the shape of a provider-specific mail transport. It is a local extension authoring task. MyronCMS core currently ships the smtp transport; provider transports are extension candidates.

The example uses a fictional API provider so the contract stays focused on extension shape instead of one vendor's SDK.

Prerequisites

  • An extension package exists under app/extensions/my_vendor/.
  • The extension manifest declares any scopes needed by provider setup or inspection.
  • Provider credentials are available through an approved settings path, not source code.
  • The operator can register, enable, and grant the extension.

Human Path

Create a transport class:

<?php

declare(strict_types=1);

final class MyVendorApiTransport implements MailTransportInterface
{
    public function __construct(private readonly MyronExtensionSDK $sdk)
    {
    }

    public function send(MailEnvelope $envelope): MailDispatchResult
    {
        $apiKey = $this->loadApiKey();
        if ($apiKey === '') {
            return MailDispatchResult::failed('x.my_vendor.api', 'mail_provider_not_configured');
        }

        // Build the provider request from the normalized envelope.
        $payload = [
            'to' => $envelope->to,
            'subject' => $envelope->subject,
            'html' => $envelope->htmlBody,
            'text' => $envelope->textBody,
            'from' => [
                'email' => $envelope->fromEmail,
                'name' => $envelope->fromName,
            ],
            'replyTo' => $envelope->replyTo,
        ];

        // Send the payload with an approved HTTP client/settings path.
        $providerMessageId = 'provider-message-id';

        return MailDispatchResult::ok('x.my_vendor.api', $providerMessageId);
    }

    public function describe(): array
    {
        return [
            'slug' => 'x.my_vendor.api',
            'label' => 'My Vendor API',
            'description' => 'API-based transactional mail transport.',
            'scopes' => [MyronApiScopes::ADMINISTRATION_READ],
            'capabilities' => ['api', 'html', 'text', 'tags'],
        ];
    }

    private function loadApiKey(): string
    {
        // Read from an approved settings or secret path. Do not hard-code secrets here.
        return '';
    }
}

Return it from bootstrap.php:

<?php

declare(strict_types=1);

require_once __DIR__ . '/mail/MyVendorApiTransport.php';

return [
    'mailTransports' => [
        'api' => new MyVendorApiTransport($sdk),
    ],
];

The registry validates the vendor and local slug as snake_case and registers the runtime id:

x.my_vendor.api

AI Path

An AI coding agent can create the class, update bootstrap.php, and run verification locally.

An installed MCP client cannot create files, enable the extension, grant scopes, or supply its own trust gate. After the transport loads, a scoped AI client can inspect descriptor metadata through the administration mail transport listing action.

Scopes

Separate setup scopes from send-time behavior:

ConcernDocumentation requirement
Descriptor listingRequires administration.read through /api/v1/administration/mail/transports.
Provider setup UI or actionDeclare the read/write administration scopes that setup actually needs.
Sending through saved settingsThe transport uses the normalized envelope and approved saved configuration.

If the provider adds setup actions, those actions need their own metadata, schemas, side effects, and scope gates. The transport descriptor does not replace action metadata.

Descriptor Fields

describe() should return:

FieldMeaning
slugFully qualified transport id, such as x.my_vendor.api.
labelHuman-readable provider label.
descriptionShort provider and delivery-mode summary.
scopesSetup or inspection scopes relevant to this transport.
capabilitiesSmall capability tags such as api, smtp, html, text, tags, batch, or webhooks.

Use capabilities as discovery hints. Do not document them as guaranteed behavior unless the transport implements that behavior.

Artifacts

Local files:

  • app/extensions/my_vendor/mail/MyVendorApiTransport.php
  • app/extensions/my_vendor/bootstrap.php
  • app/extensions/my_vendor/manifest.json

Runtime surfaces:

  • MailTransportInterface::send()
  • MailTransportInterface::describe()
  • MailDispatchResult::ok()
  • MailDispatchResult::failed()
  • GET /api/v1/administration/mail/transports

Safety Boundary

Do not hard-code secrets in the transport class, manifest, bootstrap file, docs examples, or tests. A provider implementation should fail closed with a stable error such as mail_provider_not_configured when settings are absent.

Do not claim that this example provider ships in core. Core v1 registers SMTP; provider transports are extension-authored.

Do not silently swallow provider failures. Return MailDispatchResult::failed() with a stable error string that callers and operators can act on.

Verification

Run:

php app/tests/verify-extension-system-foundation.php
php app/tests/verify-forms-mail-substrate.php

Then inspect the loaded descriptor as an administration.read caller:

curl -sS \
  -H "Authorization: Bearer ${MYRON_TOKEN}" \
  "https://example.test/api/v1/administration/mail/transports"

Verify:

  • the descriptor key is x.my_vendor.api,
  • the descriptor includes slug, label, description, scopes, and capabilities,
  • missing provider settings return a failed result instead of leaking or guessing secrets,
  • an under-scoped MCP caller cannot discover the listing action.

Failure Modes

FailureLikely causeSafe response
Extension load failsVendor or transport slug is not snake_case, or bootstrap has a PHP error.Fix namespace shape and run extension verification.
Transport descriptor is missingExtension is unregistered, disabled, under-granted, or did not return mailTransports.Check Installed Extensions and load errors.
Send returns mail_provider_not_configuredProvider settings or secrets are absent.Complete the approved setup path; do not put secrets in source.
Provider returns an errorRemote delivery failed or rejected the payload.Map it to MailDispatchResult::failed() with a stable error.
Descriptor visible to human but not MCP callerMCP caller lacks administration.read.Use a correctly scoped caller; do not broaden scope from the client.