|null */ private ?array $cache = null; /** * Default settings. * * @return array */ public static function defaults(): array { $defaults = array( // Master switch. When false, the MCP endpoint returns 403. 'enabled' => false, // Roles, besides administrators, whose accounts may connect an app // (OAuth, access tokens and Application Passwords alike). Empty by // default: only administrators. Read it through user_may_connect(). 'allowed_roles' => array(), // Tool groups that are exposed. Premium groups can register their own ids. 'enabled_groups' => array( 'site', 'content', 'users', 'files', 'themes', 'plugins', 'media', 'taxonomy', 'options', 'health', 'comments', 'meta', 'revisions', 'menus', 'database', 'cli', 'woocommerce', 'maintenance', 'abilities', 'elementor', 'builders' ), // Groups added after the first release, recorded once they have been // switched on for an existing install (see all()). 'groups_introduced' => self::INTRODUCED_GROUPS, // Require HTTPS for the endpoint (strongly recommended in production). // On the settings screen only where it matters: a site not served // over HTTPS, or one that has it off. 'require_https' => true, // Allow tools annotated as destructive (e.g. delete) to run at all. 'allow_destructive' => false, // Hold destructive calls until a signed-in WordPress user approves // each one. Supersedes allow_destructive: with this on, destructive // tools are reachable but every call is shown to a person first. // On for a new install ("Ask me first"), so the owner decides each // delete until they choose Off or Allow. The activator writes the // defaults only when no settings exist, so an existing site keeps // the choice it made. 'require_approval' => true, // Write an audit log of every tool call. 'logging_enabled' => true, // Per-user, per-minute request ceiling; 0 disables. Not on the // settings screen: read it through rate_limit(). 'rate_limit' => self::RATE_LIMIT, // Enable the OAuth authorization server (one-click connect from claude.ai). 'oauth_enabled' => true, // Allow dynamic client registration (RFC 7591). Required for one-click // connect, which registers again on every reconnect. Off the settings // screen unless a site has it off: read it through dcr_allowed(). 'oauth_allow_dcr' => true, // No longer read: in-chat uploads are always on, and the // gripmcp/in_chat_uploads filter is how a site turns them off (see // Ui_Widgets::enabled()). Kept so a stored value stays well-formed. 'enable_ui_widgets' => true, // Progress cards: an MCP Apps panel shown by the longer-running tools // (installs, theme drafts, search-replace, reports) with tips while // they run, then the result. Separate from the upload switch so either // can go without the other. On by default, and off the settings screen // unless a site has it off; Ui_Widgets::cards_enabled() reads it. 'enable_progress_cards' => true, ); // The paid version's own settings, added only where they are not // already defaults here, so none of these can be changed by it. if ( class_exists( Premium::class ) ) { $defaults += Premium::SETTINGS; } if ( class_exists( Paid_Debug_Logging::class ) ) { $defaults += Paid_Debug_Logging::SETTINGS; } return $defaults; } /** * Retrieve the full, defaults-merged settings array. * * @return array */ public function all(): array { if ( null === $this->cache ) { $stored = get_option( self::OPTION, array() ); $stored = is_array( $stored ) ? $stored : array(); if ( isset( $stored['enabled_groups'] ) && is_array( $stored['enabled_groups'] ) ) { $seen = isset( $stored['groups_introduced'] ) && is_array( $stored['groups_introduced'] ) ? $stored['groups_introduced'] : array(); $missing = array_values( array_diff( self::INTRODUCED_GROUPS, $seen ) ); if ( ! empty( $missing ) ) { $stored['enabled_groups'] = array_values( array_unique( array_merge( $stored['enabled_groups'], $missing ) ) ); $stored['groups_introduced'] = array_values( array_unique( array_merge( $seen, $missing ) ) ); update_option( self::OPTION, $stored, false ); } } $this->cache = wp_parse_args( $stored, self::defaults() ); } return $this->cache; } /** * Retrieve a single setting. * * @param string $key Setting key. * @param mixed $default Fallback if the key is unknown. * @return mixed */ public function get( string $key, $default = null ) { $all = $this->all(); return array_key_exists( $key, $all ) ? $all[ $key ] : $default; } /** * Persist a full or partial settings array (merged over current values). * * @param array $values New values. */ public function update( array $values ): void { $merged = wp_parse_args( $values, $this->all() ); update_option( self::OPTION, $merged, false ); $this->cache = null; } /** * Whether the MCP endpoint is active. */ public function is_enabled(): bool { return (bool) $this->get( 'enabled', false ); } /** * Roles, besides administrators, allowed to connect. * * @return array */ public function allowed_roles(): array { $roles = $this->get( 'allowed_roles', array() ); return is_array( $roles ) ? array_values( array_filter( array_map( 'strval', $roles ) ) ) : array(); } /** * Whether this account may use GripMCP at all. * * Administrators (manage_options; super admins on a multisite network) * always may. Any other account only when the site allowed its role: on a * shop or a membership site, anyone can sign up, and an account that can * sign in should not on that account alone be able to connect an AI app. * * @param \WP_User $user User. */ public function user_may_connect( \WP_User $user ): bool { if ( ! $user->exists() ) { return false; } $allowed = user_can( $user, 'manage_options' ) || ( is_multisite() && is_super_admin( $user->ID ) ) || ! empty( array_intersect( (array) $user->roles, $this->allowed_roles() ) ); /** * Filters whether an account may use GripMCP. * * @param bool $allowed Whether it may. * @param \WP_User $user The account. */ return (bool) apply_filters( 'gripmcp/user_may_connect', $allowed, $user ); } /** * Whether a given tool group is exposed. * * @param string $group Group id. */ public function is_group_enabled( string $group ): bool { $groups = (array) $this->get( 'enabled_groups', array() ); return in_array( $group, $groups, true ); } /** * Whether destructive tools are permitted. */ public function allow_destructive(): bool { return (bool) $this->get( 'allow_destructive', false ); } /** * Whether apps may register themselves for one-click connect (dynamic * client registration). * * The settings screen shows this only while a site has it off, because * one-click connect cannot work without it. A site that turned it off * before keeps that until someone turns it back on there. */ public function dcr_allowed(): bool { /** * Filters whether new apps may register for one-click connect. * * Return false to keep the apps that are connected now and refuse any * new registration. An app that is removed and added again registers * again, so it cannot reconnect either until this allows it. * * @param bool $allowed Whether registration is allowed. */ return (bool) apply_filters( 'gripmcp/oauth_allow_dcr', (bool) $this->get( 'oauth_allow_dcr', true ) ); } /** * Requests one user may send to the endpoint in a minute; 0 for no * ceiling. * * The number is no longer on the settings screen, where most people did * not know what to enter and 0 removed the ceiling. A value a site chose * there before is kept, 0 included, so taking the field away changes * nothing for it; the old default gives way to the higher one. */ public function rate_limit(): int { $stored = $this->get( 'rate_limit', self::RATE_LIMIT ); if ( ! is_numeric( $stored ) || self::OLD_RATE_LIMIT === (int) $stored ) { return self::RATE_LIMIT; } return max( 0, (int) $stored ); } }