| name | Klytos Plugin Development |
| description | Complete guide for developing Klytos CMS plugins including structure, entry points, MCP tools, admin pages, hooks, routes, and best practices. Use when creating, modifying, extending Klytos functionality, adding MCP tools, admin pages, hooks, filters, or debugging plugins. |
Klytos Plugin Development Guide
Architecture Overview
Klytos is an AI-First CMS controlled via MCP (Model Context Protocol). Plugins extend core functionality through a WordPress-inspired hook system (actions + filters) WITHOUT modifying core files.
Key principle: Every feature should be exposed as an MCP tool FIRST, admin UI second.
Plugin Identification (IMMUTABLE CONTRACT)
A Klytos plugin is identified by a directory plugins/{plugin-id}/ containing a PHP file named {plugin-id}.php with a Plugin Name: header in its docblock. This contract can NEVER change.
Minimum Viable Plugin
<?php
That's it. Klytos discovers it, lists it in admin, and allows activation.
Plugin Structure
plugins/{plugin-id}/
├── {plugin-id}.php ← REQUIRED: identification + entry point (PHP header)
├── klytos-plugin.json ← OPTIONAL: extended metadata
├── install.php ← Optional: runs on first activation
├── deactivate.php ← Optional: runs on deactivation
├── uninstall.php ← Optional: removes plugin data permanently
├── admin/ ← Optional: admin page views
├── assets/ ← Optional: CSS, JS, images (publicly accessible)
├── lang/ ← Optional: translation files
├── src/ ← Optional: PHP source classes
└── migrations/ ← Optional: data migrations
PHP Header (Canonical Identity)
The main PHP file MUST contain a docblock with at least Plugin Name:. All other fields are optional.
<?php
Extended Manifest (klytos-plugin.json)
For complex structured data that doesn't fit in a PHP header comment. The id field is NOT needed — it's derived from the directory name.
{
"permissions": ["pages.edit"],
"admin_pages": [
{
"id": "settings",
"title": "My Plugin Settings",
"icon": "P",
"position": 86
}
],
"mcp_tools": ["my_plugin_do_something"]
}
Main Plugin File — Entry Point
The {plugin-id}.php file is both the identification AND the entry point. All hooks are registered here.
<?php
klytos_add_filter('admin.sidebar_items', function (array $items): array {
$items[] = [
'id' => 'my-plugin',
'title' => 'My Plugin',
'url' => klytos_admin_url('plugins/my-plugin/admin/settings.php'),
'icon' => 'P',
'position' => 86,
];
return $items;
});
klytos_add_filter('mcp.tools_list', function (array $tools): array {
$tools[] = [
'name' => 'my_plugin_do_something',
'description' => 'Does something useful.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'param1' => ['type' => 'string', 'description' => 'First parameter.'],
],
],
];
return $tools;
});
klytos_add_filter('mcp.handle_tool', function (mixed $result, string $toolName, array $params): mixed {
if ($toolName !== 'my_plugin_do_something') {
return $result;
}
return [
'content' => [['type' => 'text', 'text' => 'Done!']],
'isError' => false,
];
}, 10);
klytos_register_translations('my-plugin', klytos_plugin_path('my-plugin', 'lang'));
klytos_add_action('page.after_save', function (array $page, string $action): void {
klytos_log('info', 'My plugin: page saved', ['slug' => $page['slug']]);
});
Registering Admin Pages
Use klytos_register_admin_page() to add sidebar items:
klytos_register_admin_page( 'my-plugin', [
'id' => 'settings',
'title' => 'My Plugin Settings',
'icon' => 'P',
'position' => 86,
'capability' => 'plugins.manage',
] );
The PHP file at plugins/my-plugin/admin/settings.php renders inside the admin layout automatically. It receives $app, $auth, $pluginId, $pageName, $manifest.
Core Service Accessors
klytos_storage() → StorageInterface (read/write encrypted data)
klytos_app() → App instance
klytos_auth() → Auth instance
klytos_config($key, $default) → Read config value (dot notation)
klytos_version() → Current Klytos version
klytos_is_admin() → True if in admin context
klytos_is_mcp() → True if in MCP context
klytos_current_user() → Current user array or null
klytos_has_permission($perm) → Permission check
klytos_log($level, $msg, $ctx) → Write to log file
Available Hooks
Page Lifecycle
page.before_save, page.after_save, page.before_delete, page.after_delete
page.content (filter) — modify page HTML content
Build Lifecycle
build.before, build.after, build.page.before, build.page.after (actions)
build.head_html, build.body_end_html (filters) — inject CSS/JS
Admin Panel
admin.sidebar_items (filter) — add menu items
admin.head, admin.footer (actions) — inject into admin HTML
admin.{page}.before, admin.{page}.after (actions) — per-page hooks
Blocks & Templates
block.before_save, block.after_save, block.rendered_html
page_template.before_save, page_template.after_save
Plugins
plugin.activated, plugin.deactivated, plugin.loaded
Internationalization (i18n)
Place JSON translation files in plugins/{plugin-id}/lang/:
plugins/my-plugin/lang/
├── en.json
└── es.json
Register them:
klytos_register_translations('my-plugin', klytos_plugin_path('my-plugin', 'lang'));
Translation file format (flat recommended):
{
"my_plugin.settings_title": "My Plugin Settings",
"my_plugin.save": "Save Changes"
}
Use translations:
echo __('my_plugin.settings_title');
echo __('my_plugin.greeting', ['name' => 'Jose']);
Plugin Assets (CSS, JS, Images)
Plugin static assets live in plugins/{plugin-id}/assets/ and are publicly accessible via the web.
Building Asset URLs
klytos_plugin_url('my-plugin', 'assets/css/style.css')
$cssUrl = klytos_plugin_url('my-plugin', 'assets/css/style.css');
?>
<link rel="stylesheet" href="<?php echo klytos_esc_url($cssUrl); ?>" nonce="<?php echo klytos_esc_attr($cspNonce); ?>">
<script src="<?php echo klytos_esc_url($jsUrl); ?>" nonce="<?php echo klytos_esc_attr($cspNonce); ?>"></script>
CRITICAL: CSP Nonce Requirement
All <script> and <link> tags MUST include a nonce attribute:
<script src="..." nonce="<?php echo klytos_esc_attr($cspNonce); ?>"></script>
<script src="..."></script>
Plugin Logging
Plugins opt into logging by declaring Logs: true in the PHP header. When declared, an "Enable Logs" action appears in the plugin management page.
Writing logs:
klytos_log('info', 'Order processed', ['order_id' => 42], 'my-plugin');
klytos_log_error('Payment failed', ['gateway' => 'stripe'], 'my-plugin');
klytos_log_warning('Rate limit approaching', [], 'my-plugin');
klytos_log_info('Cache refreshed', [], 'my-plugin');
Storage Pattern for Plugin Data
$storage = klytos_storage();
$storage->write('my-plugin-data', 'settings', [
'api_key' => 'xxx',
'enabled' => true,
]);
$data = $storage->read('my-plugin-data', 'settings');
Security Requirements
- Never access the filesystem directly — use
klytos_storage()
- Always sanitize HTML output — use
htmlspecialchars() or Helpers::sanitizeHtml()
- Always validate input — check types, lengths, and formats
- Use capabilities for access control — register via
auth.capabilities filter
- Never store secrets in cleartext — declare sensitivity with
klytos_register_option()
- Include the GPL-3.0-or-later license header in all PHP files if distributing
Declaring Option Sensitivity
When your plugin stores options, classify them by sensitivity so Klytos encrypts them appropriately based on the site's encryption level:
klytos_register_option('my-plugin.api_key', true);
klytos_register_option('my-plugin.webhook_secret', true);
klytos_register_option('my-plugin.user_email', 'user_data');
klytos_register_option('my-plugin.theme_color');
| Sensitivity | Encrypted at | Use for |
|---|
true | Always (all levels) | API keys, tokens, passwords, secrets |
'user_data' | Medium + Professional | Emails, IPs, personal data (GDPR) |
false (default) | Professional only | Colors, toggles, non-sensitive config |
See the klytos-options-storage skill for full documentation.
Troubleshooting
Error: "Requires Klytos X+, current: X-beta.Y"
In semver, pre-release versions are lower than the release:
Always set Requires Klytos to the OLDEST version you actually need, not the current one.
Error: Plugin assets return 403 (Forbidden)
The .htaccess in the plugins/ directory blocks access to executable files. Plugin PHP files are only executed server-side by the PluginLoader (require_once), never accessed directly via URL.
Error: Plugin JS blocked by Content-Security-Policy
The <script> tag is missing the CSP nonce attribute. Always include nonce="<?php echo klytos_esc_attr($cspNonce); ?>"
File Locations
- Klytos root:
/installer/ (configurable)
- Core:
/installer/core/
- Plugins:
/installer/plugins/
- Admin:
/installer/admin/
- Data (encrypted):
/installer/data/
- Config:
/installer/config/
For advanced topics like premium licensing, webhooks, version requirements, and more, see the references/advanced-features.md file.