Every file under src/ exits outside WordPress (defined('ABSPATH') ||
exit; after the use block, so a direct request no longer prints a fatal
error with the server path); divi-convert-fixture.php gets the WP_CLI
guard the other integration scripts have. A unit test keeps both true;
PSR1 side-effects warning excluded for the guard.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
106 lines
3.6 KiB
PHP
106 lines
3.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Abilities;
|
|
|
|
use LIT\MCP\Access;
|
|
use LIT\MCP\Integrations\IntegrationRegistry;
|
|
use LIT\MCP\Log;
|
|
use LIT\MCP\Server\RequestContext;
|
|
use Throwable;
|
|
use WP_Error;
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
/**
|
|
* Registers all abilities of available integrations with the Abilities API and
|
|
* wraps them with the role gate and neutral error handling.
|
|
*/
|
|
final class AbilityRegistrar
|
|
{
|
|
public const CATEGORY = 'lit-mcp';
|
|
|
|
public function __construct(private IntegrationRegistry $registry, private ?ExposurePolicy $policy = null)
|
|
{
|
|
}
|
|
|
|
public function register(): void
|
|
{
|
|
add_action('wp_abilities_api_categories_init', [$this, 'registerCategory']);
|
|
add_action('wp_abilities_api_init', [$this, 'registerAbilities']);
|
|
}
|
|
|
|
public function registerCategory(): void
|
|
{
|
|
wp_register_ability_category(self::CATEGORY, [
|
|
'label' => 'LIT MCP',
|
|
'description' => 'Website-Pflege für WordPress, Divi und Pods über MCP.',
|
|
]);
|
|
}
|
|
|
|
public function registerAbilities(): void
|
|
{
|
|
$policy = $this->policy ?? new ExposurePolicy();
|
|
foreach ($policy->enabled($this->registry->abilities()) as $ability) {
|
|
wp_register_ability($ability->name(), $this->argsFor($ability));
|
|
}
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
public function argsFor(Ability $ability): array
|
|
{
|
|
$args = [
|
|
'label' => $ability->label(),
|
|
'description' => $ability->description(),
|
|
'category' => self::CATEGORY,
|
|
'input_schema' => $ability->inputSchema(),
|
|
'permission_callback' => static function (mixed $input = null) use ($ability): bool|WP_Error {
|
|
if (! Access::currentUserMayUse()) {
|
|
return Errors::forbidden('Dein Benutzerkonto ist für LIT MCP nicht freigegeben.');
|
|
}
|
|
|
|
if (! RequestContext::isOAuthServerRequest()) {
|
|
return Errors::forbidden('LIT MCP ist nur über den OAuth-geschützten MCP-Server nutzbar.');
|
|
}
|
|
|
|
try {
|
|
return $ability->permission(is_array($input) ? $input : []);
|
|
} catch (Throwable $e) {
|
|
Log::failure($ability->name(), $e);
|
|
return Errors::internal();
|
|
}
|
|
},
|
|
'execute_callback' => static function (mixed $input = null) use ($ability): mixed {
|
|
try {
|
|
return $ability->execute(is_array($input) ? $input : []);
|
|
} catch (WriteFailed $e) {
|
|
Log::failure($ability->name(), $e);
|
|
return $e->toError();
|
|
} catch (Throwable $e) {
|
|
Log::failure($ability->name(), $e);
|
|
return Errors::internal();
|
|
}
|
|
},
|
|
'meta' => [
|
|
'annotations' => $ability->annotations(),
|
|
'show_in_rest' => false,
|
|
// mcp.public only governs the adapter's meta-tools (discover/get-info/execute);
|
|
// direct tools are listed on the server explicitly. Direct-only abilities
|
|
// (image/resource results) would not work through execute, so hide them there.
|
|
'mcp' => [
|
|
'public' => ! in_array($ability->name(), ExposurePolicy::DIRECT_ONLY, true),
|
|
'type' => 'tool',
|
|
],
|
|
],
|
|
];
|
|
|
|
$output = $ability->outputSchema();
|
|
if ($output !== null) {
|
|
$args['output_schema'] = $output;
|
|
}
|
|
|
|
return $args;
|
|
}
|
|
}
|