*/ private array $tools = []; /** @var array */ private array $builtin = []; /** @var list */ private array $refused = []; /** @param bool $builtin true only for the tools Aiwend itself ships; everything else is another plugin's code and is treated as untrusted */ public function add(McpTool $tool, bool $builtin = false): bool { $name = $tool->name(); if (preg_match(self::NAME_PATTERN, $name) !== 1) { return $this->refuse($name, 'invalid_name'); } if (isset($this->tools[$name])) { return $this->refuse($name, 'name_taken'); } if ($tool->mutates()) { return $this->refuse($name, 'state_changing_tools_not_offered'); } if (!$builtin && ($tool->operation() !== 'mcp.read' || count($tool->schema()) > 10 || mb_strlen($tool->description()) > 500 || mb_strlen($tool->title()) > 100)) { return $this->refuse($name, 'outside_the_third_party_contract'); } if (self::inputSchema($tool->schema()) === null) { return $this->refuse($name, 'unsupported_schema'); } $this->tools[$name] = $tool; if ($builtin) { $this->builtin[$name] = true; } return true; } public function isBuiltin(string $name): bool { return isset($this->builtin[$name]); } /** @return array sorted by name */ public function all(): array { $tools = $this->tools; ksort($tools); return $tools; } public function get(string $name): ?McpTool { return $this->tools[$name] ?? null; } /** @return list */ public function refused(): array { return $this->refused; } /** * JSON Schema (draft 2020-12 subset) for the arguments of a tool, or null if its schema uses something this catalog cannot describe. * * @param array> $schema * @return array|null */ public static function inputSchema(array $schema): ?array { $properties = []; $required = []; foreach ($schema as $key => $rule) { $type = $rule['type'] ?? ''; if (!is_string($key) || !in_array($type, ['int', 'string'], true)) { return null; } $p = ['type' => $type === 'int' ? 'integer' : 'string']; if ($type === 'int') { isset($rule['min']) && $p['minimum'] = (int) $rule['min']; isset($rule['max']) && $p['maximum'] = (int) $rule['max']; } else { $p['maxLength'] = (int) ($rule['max'] ?? 500); if (isset($rule['enum'])) { $p['enum'] = array_values((array) $rule['enum']); } } if (isset($rule['description'])) { $p['description'] = (string) $rule['description']; } $properties[$key] = $p; if (empty($rule['optional'])) { $required[] = $key; } } $out = ['type' => 'object', 'properties' => $properties === [] ? new \stdClass() : $properties, 'additionalProperties' => false]; if ($required !== []) { $out['required'] = $required; } return $out; } private function refuse(string $name, string $reason): bool { $this->refused[] = ['name' => mb_substr($name, 0, 64), 'reason' => $reason]; return false; } }