WordPress MCP Server

Project

WordPress MCP Server

A self-hosted MCP server that lets me build and manage this whole site by talking to an AI assistant — no clicking around wp-admin.

  • WordPress
  • PHP
  • MCP
  • mcp-adapter
  • Abilities API

Why I built it

I wanted to drive my WordPress site from an AI assistant — drafting posts, building pages, tweaking the design — without living in wp-admin. There are paid services that do this, but they route your site through their own servers and meter usage. I self-host on purpose, and I didn’t love the idea of a third party holding my keys or charging per action.

So I built my own. It’s free, fully self-hosted (nothing leaves my VPS), and I control exactly which tools exist and what they’re allowed to do. It runs on the official WordPress MCP Adapter and the Abilities API (now in WordPress core), with a small must-use plugin that registers the tools I want and exposes them as an MCP server.

How it works

The MCP Adapter bridges WordPress’s Abilities API to the Model Context Protocol. My plugin registers abilities — create and update content, upload media, manage menus and taxonomy, change settings and CSS, even the footer — and exposes them as a custom MCP server at a REST endpoint. The AI client connects over HTTPS with a WordPress Application Password, so it can only do what that user can. No delete tools unless I add them — least privilege by default.

The code

Here’s the whole thing — the full must-use plugin that runs this site, fourteen tools all registered the same way. Personal naming is swapped for generic placeholders; drop it into wp-content/mu-plugins/ and change the namespace to your own.

<?php
/**
 * Plugin Name: MCP Content Abilities
 * Description: Registers site-building abilities and exposes them as MCP tools via the WordPress MCP Adapter.
 * Version:     2.0.0
 * Author:      Your Name
 *
 * Drop this file in wp-content/mu-plugins/ (it auto-loads, no activation needed).
 * Requires: WordPress 6.9+ (Abilities API in core) and the WordPress/mcp-adapter plugin active.
 * Endpoint it creates: /wp-json/site-mcp/mcp
 *
 * Tools: list/get/create/update/delete content, upload media (from URL),
 *        set featured image, list/create/assign terms, update site settings
 *        (incl. static homepage), set custom CSS, set navigation.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/* -------------------------------------------------------------------------
 * 1. Ability category
 * ---------------------------------------------------------------------- */
add_action( 'wp_abilities_api_categories_init', function () {
	wp_register_ability_category( 'site-content', array(
		'label'       => __( 'Site building', 'site-mcp' ),
		'description' => __( 'Read and write content, media, taxonomy, settings, design, and navigation.', 'site-mcp' ),
	) );
} );

/* -------------------------------------------------------------------------
 * 2. Abilities
 * ---------------------------------------------------------------------- */
add_action( 'wp_abilities_api_init', function () {

	/* ---- Content ----------------------------------------------------- */

	wp_register_ability( 'site/list-content', array(
		'label'        => __( 'List content', 'site-mcp' ),
		'description'  => __( 'List posts or pages, optionally filtered by status or a search term.', 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'properties' => array(
				'post_type'   => array( 'type' => 'string', 'enum' => array( 'post', 'page' ), 'default' => 'post', 'description' => 'Which content type to list.' ),
				'post_status' => array( 'type' => 'string', 'default' => 'any', 'description' => 'publish, draft, pending, private, or any.' ),
				'search'      => array( 'type' => 'string', 'description' => 'Optional search term.' ),
				'number'      => array( 'type' => 'integer', 'default' => 20, 'minimum' => 1, 'maximum' => 100, 'description' => 'Max items to return.' ),
			),
		),
		'output_schema'       => array( 'type' => 'array' ),
		'execute_callback'    => 'site_mcp_list_content',
		'permission_callback' => 'site_mcp_can_edit',
	) );

	wp_register_ability( 'site/get-content', array(
		'label'        => __( 'Get content', 'site-mcp' ),
		'description'  => __( 'Retrieve a single post or page by ID, including its full content.', 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'required'   => array( 'id' ),
			'properties' => array( 'id' => array( 'type' => 'integer', 'description' => 'The post or page ID.' ) ),
		),
		'output_schema'       => array( 'type' => 'object' ),
		'execute_callback'    => 'site_mcp_get_content',
		'permission_callback' => 'site_mcp_can_edit',
	) );

	wp_register_ability( 'site/create-content', array(
		'label'        => __( 'Create content', 'site-mcp' ),
		'description'  => __( 'Create a new post or page. Defaults to a draft post. Content accepts HTML or block markup.', 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'required'   => array( 'title' ),
			'properties' => array(
				'title'     => array( 'type' => 'string', 'description' => 'The title.' ),
				'content'   => array( 'type' => 'string', 'description' => 'Body content (HTML or block markup).' ),
				'excerpt'   => array( 'type' => 'string', 'description' => 'Optional excerpt.' ),
				'post_type' => array( 'type' => 'string', 'enum' => array( 'post', 'page' ), 'default' => 'post' ),
				'status'    => array( 'type' => 'string', 'enum' => array( 'draft', 'publish', 'pending', 'private' ), 'default' => 'draft' ),
			),
		),
		'output_schema'       => array( 'type' => 'object' ),
		'execute_callback'    => 'site_mcp_create_content',
		'permission_callback' => 'site_mcp_can_edit',
	) );

	wp_register_ability( 'site/update-content', array(
		'label'        => __( 'Update content', 'site-mcp' ),
		'description'  => __( 'Update an existing post or page by ID. Only the fields you pass are changed.', 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'required'   => array( 'id' ),
			'properties' => array(
				'id'      => array( 'type' => 'integer', 'description' => 'The post or page ID to update.' ),
				'title'   => array( 'type' => 'string' ),
				'content' => array( 'type' => 'string' ),
				'excerpt' => array( 'type' => 'string' ),
				'status'  => array( 'type' => 'string', 'enum' => array( 'draft', 'publish', 'pending', 'private' ) ),
				'slug'    => array( 'type' => 'string' ),
			),
		),
		'output_schema'       => array( 'type' => 'object' ),
		'execute_callback'    => 'site_mcp_update_content',
		'permission_callback' => 'site_mcp_can_edit',
	) );

	wp_register_ability( 'site/delete-content', array(
		'label'        => __( 'Delete content', 'site-mcp' ),
		'description'  => __( 'Move a post or page to the trash (reversible). Does not permanently delete.', 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'required'   => array( 'id' ),
			'properties' => array( 'id' => array( 'type' => 'integer', 'description' => 'The post or page ID to trash.' ) ),
		),
		'output_schema'       => array( 'type' => 'object' ),
		'execute_callback'    => 'site_mcp_delete_content',
		'permission_callback' => 'site_mcp_can_delete',
	) );

	/* ---- Media ------------------------------------------------------- */

	wp_register_ability( 'site/upload-media', array(
		'label'        => __( 'Upload media from URL', 'site-mcp' ),
		'description'  => __( 'Download an image from a URL into the media library. Returns the attachment ID and URL.', 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'required'   => array( 'url' ),
			'properties' => array(
				'url'   => array( 'type' => 'string', 'description' => 'Source image URL (https).' ),
				'title' => array( 'type' => 'string', 'description' => 'Optional title for the attachment.' ),
				'alt'   => array( 'type' => 'string', 'description' => 'Optional alt text.' ),
			),
		),
		'output_schema'       => array( 'type' => 'object' ),
		'execute_callback'    => 'site_mcp_upload_media',
		'permission_callback' => 'site_mcp_can_upload',
	) );

	wp_register_ability( 'site/set-featured-image', array(
		'label'        => __( 'Set featured image', 'site-mcp' ),
		'description'  => __( 'Set the featured image of a post or page to an existing media attachment.', 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'required'   => array( 'post_id', 'attachment_id' ),
			'properties' => array(
				'post_id'       => array( 'type' => 'integer', 'description' => 'The post or page ID.' ),
				'attachment_id' => array( 'type' => 'integer', 'description' => 'The media attachment ID.' ),
			),
		),
		'output_schema'       => array( 'type' => 'object' ),
		'execute_callback'    => 'site_mcp_set_featured_image',
		'permission_callback' => 'site_mcp_can_edit',
	) );

	/* ---- Taxonomy ---------------------------------------------------- */

	wp_register_ability( 'site/list-terms', array(
		'label'        => __( 'List terms', 'site-mcp' ),
		'description'  => __( 'List categories or tags.', 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'properties' => array(
				'taxonomy' => array( 'type' => 'string', 'enum' => array( 'category', 'post_tag' ), 'default' => 'category' ),
			),
		),
		'output_schema'       => array( 'type' => 'array' ),
		'execute_callback'    => 'site_mcp_list_terms',
		'permission_callback' => 'site_mcp_can_edit',
	) );

	wp_register_ability( 'site/create-term', array(
		'label'        => __( 'Create term', 'site-mcp' ),
		'description'  => __( 'Create a category or tag.', 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'required'   => array( 'name' ),
			'properties' => array(
				'name'        => array( 'type' => 'string' ),
				'taxonomy'    => array( 'type' => 'string', 'enum' => array( 'category', 'post_tag' ), 'default' => 'category' ),
				'description' => array( 'type' => 'string' ),
			),
		),
		'output_schema'       => array( 'type' => 'object' ),
		'execute_callback'    => 'site_mcp_create_term',
		'permission_callback' => 'site_mcp_can_manage',
	) );

	wp_register_ability( 'site/assign-terms', array(
		'label'        => __( 'Assign terms', 'site-mcp' ),
		'description'  => __( 'Assign categories or tags (by name) to a post. Replaces existing terms for that taxonomy.', 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'required'   => array( 'post_id', 'terms' ),
			'properties' => array(
				'post_id'  => array( 'type' => 'integer' ),
				'taxonomy' => array( 'type' => 'string', 'enum' => array( 'category', 'post_tag' ), 'default' => 'category' ),
				'terms'    => array( 'type' => 'array', 'items' => array( 'type' => 'string' ), 'description' => 'Term names. Missing ones are created.' ),
			),
		),
		'output_schema'       => array( 'type' => 'object' ),
		'execute_callback'    => 'site_mcp_assign_terms',
		'permission_callback' => 'site_mcp_can_edit',
	) );

	/* ---- Site settings, design, navigation --------------------------- */

	wp_register_ability( 'site/update-site-settings', array(
		'label'        => __( 'Update site settings', 'site-mcp' ),
		'description'  => __( 'Update site title, tagline, and the static front page / posts page.', 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'properties' => array(
				'title'         => array( 'type' => 'string', 'description' => 'Site title (blogname).' ),
				'tagline'       => array( 'type' => 'string', 'description' => 'Tagline (blogdescription).' ),
				'front_page_id' => array( 'type' => 'integer', 'description' => 'Page ID to use as the static homepage. Sets show_on_front to "page".' ),
				'posts_page_id' => array( 'type' => 'integer', 'description' => 'Page ID to use as the blog/posts page.' ),
			),
		),
		'output_schema'       => array( 'type' => 'object' ),
		'execute_callback'    => 'site_mcp_update_site_settings',
		'permission_callback' => 'site_mcp_can_manage',
	) );

	wp_register_ability( 'site/set-custom-css', array(
		'label'        => __( 'Set custom CSS', 'site-mcp' ),
		'description'  => __( "Replace the active theme's Additional CSS (the Customizer custom CSS).", 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'required'   => array( 'css' ),
			'properties' => array( 'css' => array( 'type' => 'string', 'description' => 'The full CSS to store (replaces existing).' ) ),
		),
		'output_schema'       => array( 'type' => 'object' ),
		'execute_callback'    => 'site_mcp_set_custom_css',
		'permission_callback' => 'site_mcp_can_manage',
	) );

	wp_register_ability( 'site/set-navigation', array(
		'label'        => __( 'Set navigation', 'site-mcp' ),
		'description'  => __( "Set the site's primary navigation (block-theme wp_navigation) to a list of links. Replaces the current menu.", 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'required'   => array( 'items' ),
			'properties' => array(
				'items' => array(
					'type'        => 'array',
					'description' => 'Ordered nav items.',
					'items'       => array(
						'type'       => 'object',
						'required'   => array( 'label' ),
						'properties' => array(
							'label'   => array( 'type' => 'string' ),
							'page_id' => array( 'type' => 'integer', 'description' => 'Link to this page ID.' ),
							'url'     => array( 'type' => 'string', 'description' => 'Or a custom URL (used if no page_id).' ),
						),
					),
				),
			),
		),
		'output_schema'       => array( 'type' => 'object' ),
		'execute_callback'    => 'site_mcp_set_navigation',
		'permission_callback' => 'site_mcp_can_manage',
	) );

	wp_register_ability( 'site/set-footer', array(
		'label'        => __( 'Set footer', 'site-mcp' ),
		'description'  => __( 'Replace the site footer (the block-theme footer template part) with the given block markup.', 'site-mcp' ),
		'category'     => 'site-content',
		'input_schema' => array(
			'type'       => 'object',
			'required'   => array( 'content' ),
			'properties' => array( 'content' => array( 'type' => 'string', 'description' => 'Full block markup for the footer (replaces the current footer).' ) ),
		),
		'output_schema'       => array( 'type' => 'object' ),
		'execute_callback'    => 'site_mcp_set_footer',
		'permission_callback' => 'site_mcp_can_manage',
	) );
} );

/* -------------------------------------------------------------------------
 * 3. Expose the abilities as tools on a dedicated MCP server.
 *    Endpoint: /wp-json/site-mcp/mcp
 * ---------------------------------------------------------------------- */
add_action( 'mcp_adapter_init', function ( $adapter ) {
	$adapter->create_server(
		'site-mcp-server',
		'site-mcp',
		'mcp',
		'Site Content MCP',
		'Build & manage example.com: content, media, taxonomy, settings, design, navigation',
		'v2.0.0',
		array( \WP\MCP\Transport\HttpTransport::class ),
		\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
		\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
		array(
			'site/list-content',
			'site/get-content',
			'site/create-content',
			'site/update-content',
			'site/delete-content',
			'site/upload-media',
			'site/set-featured-image',
			'site/list-terms',
			'site/create-term',
			'site/assign-terms',
			'site/update-site-settings',
			'site/set-custom-css',
			'site/set-navigation',
			'site/set-footer',
		),
		array(),
		array()
	);
} );

/* -------------------------------------------------------------------------
 * 4. Permission checks.
 * ---------------------------------------------------------------------- */
function site_mcp_can_edit() {
	return current_user_can( 'edit_posts' );
}
function site_mcp_can_delete() {
	return current_user_can( 'delete_posts' );
}
function site_mcp_can_upload() {
	return current_user_can( 'upload_files' );
}
function site_mcp_can_manage() {
	return current_user_can( 'manage_options' );
}

/* -------------------------------------------------------------------------
 * 5. Callbacks.
 * ---------------------------------------------------------------------- */
function site_mcp_list_content( array $input = array() ): array {
	$query = new WP_Query( array(
		'post_type'      => $input['post_type'] ?? 'post',
		'post_status'    => $input['post_status'] ?? 'any',
		's'              => $input['search'] ?? '',
		'posts_per_page' => $input['number'] ?? 20,
		'no_found_rows'  => true,
	) );

	$out = array();
	foreach ( $query->posts as $post ) {
		$out[] = array(
			'id'     => (int) $post->ID,
			'title'  => get_the_title( $post ),
			'status' => $post->post_status,
			'type'   => $post->post_type,
			'link'   => (string) get_permalink( $post ),
			'date'   => $post->post_date_gmt,
		);
	}
	return $out;
}

function site_mcp_get_content( array $input = array() ) {
	$post = get_post( (int) ( $input['id'] ?? 0 ) );
	if ( ! $post ) {
		return new WP_Error( 'not_found', 'No post or page with that ID.' );
	}
	return array(
		'id'      => (int) $post->ID,
		'title'   => $post->post_title,
		'content' => $post->post_content,
		'excerpt' => $post->post_excerpt,
		'status'  => $post->post_status,
		'type'    => $post->post_type,
		'slug'    => $post->post_name,
		'link'    => (string) get_permalink( $post ),
		'date'    => $post->post_date_gmt,
	);
}

function site_mcp_create_content( array $input = array() ) {
	$id = wp_insert_post( array(
		'post_title'   => $input['title'] ?? '',
		'post_content' => $input['content'] ?? '',
		'post_excerpt' => $input['excerpt'] ?? '',
		'post_type'    => $input['post_type'] ?? 'post',
		'post_status'  => $input['status'] ?? 'draft',
	), true );

	if ( is_wp_error( $id ) ) {
		return $id;
	}
	$post = get_post( $id );
	return array(
		'id'     => (int) $post->ID,
		'link'   => (string) get_permalink( $post ),
		'status' => $post->post_status,
	);
}

function site_mcp_update_content( array $input = array() ) {
	$id = (int) ( $input['id'] ?? 0 );
	if ( ! get_post( $id ) ) {
		return new WP_Error( 'not_found', 'No post or page with that ID.' );
	}

	$data = array( 'ID' => $id );
	$map  = array(
		'title'   => 'post_title',
		'content' => 'post_content',
		'excerpt' => 'post_excerpt',
		'status'  => 'post_status',
		'slug'    => 'post_name',
	);
	foreach ( $map as $in => $field ) {
		if ( isset( $input[ $in ] ) ) {
			$data[ $field ] = $input[ $in ];
		}
	}

	$result = wp_update_post( $data, true );
	if ( is_wp_error( $result ) ) {
		return $result;
	}
	$post = get_post( $id );
	return array(
		'id'     => (int) $post->ID,
		'link'   => (string) get_permalink( $post ),
		'status' => $post->post_status,
	);
}

function site_mcp_delete_content( array $input = array() ) {
	$id = (int) ( $input['id'] ?? 0 );
	if ( ! get_post( $id ) ) {
		return new WP_Error( 'not_found', 'No post or page with that ID.' );
	}
	$result = wp_trash_post( $id );
	if ( ! $result ) {
		return new WP_Error( 'trash_failed', 'Could not move the item to trash.' );
	}
	return array( 'id' => $id, 'status' => 'trash' );
}

function site_mcp_upload_media( array $input = array() ) {
	$url = $input['url'] ?? '';
	if ( ! $url ) {
		return new WP_Error( 'no_url', 'A url is required.' );
	}

	require_once ABSPATH . 'wp-admin/includes/media.php';
	require_once ABSPATH . 'wp-admin/includes/file.php';
	require_once ABSPATH . 'wp-admin/includes/image.php';

	$tmp = download_url( $url );
	if ( is_wp_error( $tmp ) ) {
		return $tmp;
	}

	$name       = basename( wp_parse_url( $url, PHP_URL_PATH ) );
	$file_array = array(
		'name'     => $name ? $name : 'image.jpg',
		'tmp_name' => $tmp,
	);

	$attachment_id = media_handle_sideload( $file_array, 0, $input['title'] ?? null );
	if ( is_wp_error( $attachment_id ) ) {
		@unlink( $tmp );
		return $attachment_id;
	}

	if ( ! empty( $input['alt'] ) ) {
		update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $input['alt'] ) );
	}

	return array(
		'id'  => (int) $attachment_id,
		'url' => (string) wp_get_attachment_url( $attachment_id ),
	);
}

function site_mcp_set_featured_image( array $input = array() ) {
	$post_id       = (int) ( $input['post_id'] ?? 0 );
	$attachment_id = (int) ( $input['attachment_id'] ?? 0 );
	if ( ! get_post( $post_id ) ) {
		return new WP_Error( 'not_found', 'No post or page with that ID.' );
	}
	set_post_thumbnail( $post_id, $attachment_id );
	return array( 'post_id' => $post_id, 'attachment_id' => $attachment_id );
}

function site_mcp_list_terms( array $input = array() ): array {
	$terms = get_terms( array(
		'taxonomy'   => $input['taxonomy'] ?? 'category',
		'hide_empty' => false,
	) );
	$out = array();
	if ( ! is_wp_error( $terms ) ) {
		foreach ( $terms as $t ) {
			$out[] = array( 'id' => (int) $t->term_id, 'name' => $t->name, 'slug' => $t->slug, 'count' => (int) $t->count );
		}
	}
	return $out;
}

function site_mcp_create_term( array $input = array() ) {
	$res = wp_insert_term(
		$input['name'] ?? '',
		$input['taxonomy'] ?? 'category',
		array( 'description' => $input['description'] ?? '' )
	);
	if ( is_wp_error( $res ) ) {
		return $res;
	}
	return array( 'id' => (int) $res['term_id'], 'name' => $input['name'] ?? '' );
}

function site_mcp_assign_terms( array $input = array() ) {
	$post_id  = (int) ( $input['post_id'] ?? 0 );
	$taxonomy = $input['taxonomy'] ?? 'category';
	$terms    = $input['terms'] ?? array();
	if ( ! get_post( $post_id ) ) {
		return new WP_Error( 'not_found', 'No post or page with that ID.' );
	}
	$res = wp_set_object_terms( $post_id, $terms, $taxonomy, false );
	if ( is_wp_error( $res ) ) {
		return $res;
	}
	return array( 'post_id' => $post_id, 'taxonomy' => $taxonomy, 'assigned' => array_values( $terms ) );
}

function site_mcp_update_site_settings( array $input = array() ) {
	if ( isset( $input['title'] ) ) {
		update_option( 'blogname', sanitize_text_field( $input['title'] ) );
	}
	if ( isset( $input['tagline'] ) ) {
		update_option( 'blogdescription', sanitize_text_field( $input['tagline'] ) );
	}
	if ( ! empty( $input['front_page_id'] ) ) {
		update_option( 'show_on_front', 'page' );
		update_option( 'page_on_front', (int) $input['front_page_id'] );
	}
	if ( ! empty( $input['posts_page_id'] ) ) {
		update_option( 'page_for_posts', (int) $input['posts_page_id'] );
	}
	return array(
		'title'         => get_option( 'blogname' ),
		'tagline'       => get_option( 'blogdescription' ),
		'show_on_front' => get_option( 'show_on_front' ),
		'page_on_front' => (int) get_option( 'page_on_front' ),
		'page_for_posts'=> (int) get_option( 'page_for_posts' ),
	);
}

function site_mcp_set_custom_css( array $input = array() ) {
	$css = $input['css'] ?? '';
	$res = wp_update_custom_css_post( $css );
	if ( is_wp_error( $res ) ) {
		return $res;
	}
	return array( 'ok' => true, 'bytes' => strlen( $css ), 'stylesheet' => get_stylesheet() );
}

function site_mcp_set_navigation( array $input = array() ) {
	$items = $input['items'] ?? array();
	if ( empty( $items ) || ! is_array( $items ) ) {
		return new WP_Error( 'no_items', 'Provide at least one nav item.' );
	}

	$inner = '';
	foreach ( $items as $it ) {
		$label = isset( $it['label'] ) ? $it['label'] : '';
		if ( ! empty( $it['page_id'] ) ) {
			$pid  = (int) $it['page_id'];
			$href = get_permalink( $pid );
			$attrs = array( 'label' => $label, 'type' => 'page', 'id' => $pid, 'url' => $href, 'kind' => 'post-type' );
		} else {
			$attrs = array( 'label' => $label, 'url' => $it['url'] ?? '#', 'kind' => 'custom' );
		}
		$inner .= '<!-- wp:navigation-link ' . wp_json_encode( $attrs ) . ' /-->';
	}

	$existing = get_posts( array(
		'post_type'      => 'wp_navigation',
		'posts_per_page' => 1,
		'post_status'    => 'publish',
		'orderby'        => 'ID',
		'order'          => 'ASC',
	) );

	if ( $existing ) {
		$nav_id = $existing[0]->ID;
		wp_update_post( array( 'ID' => $nav_id, 'post_content' => $inner ) );
	} else {
		$nav_id = wp_insert_post( array(
			'post_type'    => 'wp_navigation',
			'post_status'  => 'publish',
			'post_title'   => 'Navigation',
			'post_content' => $inner,
		) );
	}

	return array( 'navigation_id' => (int) $nav_id, 'items' => count( $items ) );
}

function site_mcp_set_footer( array $input = array() ) {
	$content = $input['content'] ?? '';
	if ( '' === trim( $content ) ) {
		return new WP_Error( 'no_content', 'Footer content is required.' );
	}
	$theme = get_stylesheet();

	$existing = get_posts( array(
		'post_type'      => 'wp_template_part',
		'name'           => 'footer',
		'post_status'    => 'any',
		'posts_per_page' => 1,
		'tax_query'      => array( array( 'taxonomy' => 'wp_theme', 'field' => 'name', 'terms' => $theme ) ),
	) );

	if ( $existing ) {
		$id = $existing[0]->ID;
		wp_update_post( array( 'ID' => $id, 'post_content' => $content ) );
	} else {
		$id = wp_insert_post( array(
			'post_type'    => 'wp_template_part',
			'post_name'    => 'footer',
			'post_title'   => 'Footer',
			'post_status'  => 'publish',
			'post_content' => $content,
		), true );
		if ( is_wp_error( $id ) ) {
			return $id;
		}
		wp_set_object_terms( $id, $theme, 'wp_theme' );
		wp_set_object_terms( $id, 'footer', 'wp_template_part_area' );
	}
	return array( 'template_part_id' => (int) $id, 'theme' => $theme );
}

Install it yourself

  1. Run WordPress 6.9+ — the Abilities API ships in core.
  2. Install the official WordPress/mcp-adapter plugin from its GitHub releases.
  3. Drop the plugin above into wp-content/mu-plugins/ — it auto-loads, no activation needed.
  4. Create an Application Password under Users → Profile, and copy it.
  5. Confirm the server registered: wp mcp-adapter list.

Connect Claude

In Claude Desktop, open Settings → Developer → Edit Config and add this to claude_desktop_config.json (you’ll need Node.js 22+). It runs a small local proxy that connects to your site’s REST endpoint with your application password:

{
  "mcpServers": {
    "my-site": {
      "command": "npx",
      "args": ["-y", "@automattic/mcp-wordpress-remote@latest"],
      "env": {
        "WP_API_URL": "https://example.com/wp-json/site-mcp/mcp",
        "WP_API_USERNAME": "your-wp-username",
        "WP_API_PASSWORD": "your application password"
      }
    }
  }
}

Then fully quit and relaunch Claude Desktop — new servers only load on a restart. Ask it to “list my WordPress posts” to confirm you’re connected.

Updates

Build log

  • Shipped

    A full site-building toolkit

    Expanded to fourteen tools — media, menus, taxonomy, settings, custom CSS, navigation, and footer. I can now build and style whole pages just by talking to the assistant.

  • Built

    First working server

    Got content read and write working over MCP end to end — listing, creating, and updating posts and pages straight from chat.