feat: group tools by integration with inherited group mode and "nur lesen" switch

Tools tab shows one block per integration with a group mode (inherited by its
tools) and a read-only switch that disables all non-readonly abilities of the
group. ExposurePolicy resolves tool override > group override > tool default;
lit/guide keeps its default, direct-only tools fall back to direct under a
group "indirekt". Group entries live in lit_mcp_exposure under "@group:<id>"
and "@readonly:<id>"; the old flat format stays valid. The ability->group map
comes lazily from IntegrationRegistry (Divi is not loaded at plugins_loaded).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 16:17:05 +02:00
co-authored by Claude Opus 5.5
parent ede0221b50
commit 776dbf8f4a
10 changed files with 847 additions and 92 deletions
+56 -10
View File
@@ -11,8 +11,8 @@ final class IntegrationRegistry
/** @var list<Integration> */
private array $integrations;
/** @var list<Ability>|null */
private ?array $abilities = null;
/** @var list<array{Integration, list<Ability>}>|null Available integrations with their abilities. */
private ?array $loaded = null;
public function __construct(Integration ...$integrations)
{
@@ -31,15 +31,61 @@ final class IntegrationRegistry
/** @return list<Ability> */
public function abilities(): array
{
if ($this->abilities === null) {
$abilities = [];
foreach ($this->available() as $integration) {
array_push($abilities, ...$integration->abilities());
}
$this->abilities = $abilities;
$abilities = [];
foreach ($this->loaded() as [, $own]) {
array_push($abilities, ...$own);
}
return $this->abilities;
return $abilities;
}
/**
* Abilities of the available integrations, grouped by integration id (registry order).
*
* @return array<string, list<Ability>>
*/
public function grouped(): array
{
$groups = [];
foreach ($this->loaded() as [$integration, $own]) {
$groups[$integration->id()] = array_merge($groups[$integration->id()] ?? [], $own);
}
return $groups;
}
/** @return array<string, string> Ability name => integration id (the ability's group). */
public function groupMap(): array
{
$map = [];
foreach ($this->grouped() as $id => $abilities) {
foreach ($abilities as $ability) {
$map[$ability->name()] = $id;
}
}
return $map;
}
/** @return list<string> Ids of all known integrations, available or not. */
public function ids(): array
{
return array_values(array_unique(array_map(
static fn (Integration $integration): string => $integration->id(),
$this->integrations
)));
}
/** @return list<array{Integration, list<Ability>}> */
private function loaded(): array
{
if ($this->loaded === null) {
$this->loaded = array_map(
static fn (Integration $integration): array => [$integration, $integration->abilities()],
$this->available()
);
}
return $this->loaded;
}
}