id = 'albert/create-page'; $this->label = __( 'Create Page', 'albert-ai-butler' ); $this->description = __( 'Create a new WordPress page with specified title and content.', 'albert-ai-butler' ); $this->category = 'content'; $this->group = 'pages'; $this->input_schema = $this->get_input_schema(); $this->output_schema = $this->get_output_schema(); $this->meta = [ 'mcp' => [ 'public' => true, ], 'annotations' => Annotations::create( 'Call `albert/list-block-types` first and compose only from what it returns, an unlisted ' . 'block fails validation and nothing is saved. Send block specs in `blocks`, never ' . 'hand-written `` markup, and put child blocks in `innerBlocks` rather than ' . 'in attributes. For a classic-editor page type, send HTML in `content` instead.' ), ]; parent::__construct(); } /** * Get the input schema for this ability. * * @return array Input schema. * @since 1.0.0 */ protected function get_input_schema(): array { // Get all available post statuses dynamically. $post_statuses = array_keys( get_post_statuses() ); return [ 'type' => 'object', 'properties' => [ 'title' => [ 'type' => 'string', 'description' => 'The page title', ], 'content' => [ 'type' => 'string', 'description' => 'The page content as a string. Accepts WordPress block markup (), HTML, or Markdown — converted to blocks automatically. For multi-block, nested layouts prefer the structured "blocks" field instead. Ignored when "blocks" is provided.', 'default' => '', ], 'blocks' => [ 'type' => 'array', 'description' => 'Structured block specs (preferred over "content"). Each item is { "name": "core/paragraph", "attributes": { ... }, "innerBlocks": [ ... ] }; innerBlocks is the same shape recursively for layout blocks (e.g. core/columns > core/column). Put text in attributes (content/text/value) or in "plaintext". Example: [ { "name": "core/heading", "attributes": { "level": 2, "content": "Intro" } }, { "name": "core/paragraph", "attributes": { "content": "Hello world." } } ]', 'items' => BlockSpecSchema::spec(), 'default' => [], ], 'status' => [ 'type' => 'string', 'enum' => $post_statuses, 'description' => 'Page status', 'default' => 'draft', ], 'excerpt' => [ 'type' => 'string', 'description' => 'Optional page excerpt', 'default' => '', ], 'parent' => [ 'type' => 'integer', 'description' => 'Parent page ID for hierarchical pages', 'default' => 0, ], ], 'required' => [ 'title' ], ]; } /** * Get the output schema for this ability. * * @return array Output schema. * @since 1.0.0 */ protected function get_output_schema(): array { return [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer' ], 'title' => [ 'type' => 'string' ], 'status' => [ 'type' => 'string' ], 'permalink' => [ 'type' => 'string' ], 'edit_url' => [ 'type' => 'string' ], 'block_issues' => [ 'type' => 'array', 'description' => 'Optional, non-fatal block validation warnings (the page was still saved). Each is an actionable message such as "content[0].attributes.url is required for core/image". Fatal block problems are not returned here — they come back as a WP_Error with code "block_validation_failed" and the page is not created.', 'items' => [ 'type' => 'string' ], ], ], 'required' => [ 'id', 'title', 'status' ], ]; } /** * Check if current user has permission to execute this ability. * * Uses the permission callback from the WordPress REST API endpoint. * * @return bool|WP_Error True if permitted, WP_Error with details otherwise. * @since 1.0.0 */ public function check_permission(): bool|WP_Error { return $this->check_rest_permission( '/wp/v2/pages', 'POST', 'edit_pages' ); } /** * Execute the ability - create a page using WordPress REST API. * * @param array $args { * Input parameters. * * @type string $title Page title (required). * @type string $content Page content. * @type string $status Page status. * @type string $excerpt Page excerpt. * @type int $parent Parent page ID. * } * @return array|WP_Error Page data on success, WP_Error on failure. * @since 1.0.0 */ public function execute( array $args ): array|WP_Error { // Resolve the content to store from the content/blocks input (classic // rejection, block serialization, allowed-block enforcement, issues). $resolved = ( new WriteContentResolver() )->resolve( $args, 'page' ); if ( is_wp_error( $resolved ) ) { return $resolved; } $block_issues = $resolved['block_issues']; // Prepare REST API request data. $request_data = [ 'title' => sanitize_text_field( $args['title'] ), 'content' => $resolved['content'], 'status' => sanitize_key( $args['status'] ?? 'draft' ), 'excerpt' => sanitize_textarea_field( $args['excerpt'] ?? '' ), ]; // Add parent if provided. if ( ! empty( $args['parent'] ) ) { $request_data['parent'] = absint( $args['parent'] ); } // Create REST request. $request = new WP_REST_Request( 'POST', '/wp/v2/pages' ); foreach ( $request_data as $key => $value ) { $request->set_param( $key, $value ); } // Execute the request. $response = rest_do_request( $request ); $server = rest_get_server(); $data = $server->response_to_data( $response, false ); // Check for errors. if ( is_wp_error( $data ) ) { return $data; } if ( $response->is_error() ) { return new WP_Error( $data['code'] ?? 'rest_error', $data['message'] ?? __( 'An error occurred while creating the page.', 'albert-ai-butler' ), [ 'status' => $response->get_status() ] ); } // Return formatted page data. $page_id = $data['id']; $result = [ 'id' => $page_id, 'title' => $data['title']['rendered'] ?? '', 'status' => $data['status'], 'permalink' => $data['link'] ?? '', 'edit_url' => admin_url( 'post.php?post=' . $page_id . '&action=edit' ), ]; if ( ! empty( $block_issues ) ) { $result['block_issues'] = $block_issues; } return $result; } }