by InstaWP
Provides a Model Context Protocol server that enables natural‑language interaction with WordPress sites, exposing content, taxonomy, media, users, comments, plugins, and read‑only database queries as programmable tools.
MCP Server for WordPress supplies a set of MCP‑compatible tools that let large language models read, create, update, and delete WordPress resources using plain language commands. It works with core posts, pages, custom post types, all taxonomies, media, users, comments, plugins, and even custom SQL queries.
npx -y @instawp/mcp-wp
.env file in the working directory containing your WordPress connection details. For a single site:
WORDPRESS_API_URL=https://your-site.com
WORDPRESS_USERNAME=wp_user
WORDPRESS_PASSWORD=app_password
For multiple sites, use the numbered WORDPRESS_1_URL, WORDPRESS_2_URL, … variables as documented.logs/wordpress-api.log.build/server.js) to Claude Desktop’s Developer settings (or any MCP‑compatible client). Restart the client; the tools become available.list_sites, get_site, test_site let you control up to 10 WordPress installations from a single server instance.list_content, get_content, create_content, update_content, delete_content, discover_content_types, find_content_by_url, get_content_by_slug, get_content_summary) works with posts, pages, WooCommerce products, custom post types, and even WP Recipe Maker recipes.discover_taxonomies, list_terms, get_term, create_term, update_term, delete_term, assign_terms_to_content, get_content_terms).execute_sql_query tool.find_content_by_url automatically detects the content type from any WordPress URL and can optionally edit the found item in a single call.get_content_summary returns a fixed‑shape, low‑token payload useful for audits and lookup workflows.append, replace, and block conversion operations.MCP_WP_STRIP_FIELDS.create_media with local file paths or remote URLs.get_content_summary) to analyze word counts, focus keywords, and meta titles.execute_sql_query) for analytics that the REST API does not expose.Q: Do I need to install the package globally?
A: No. Use the provided npx command (npx -y @instawp/mcp-wp) which downloads and runs the server on demand.
Q: How do I authenticate to WordPress?
A: The server uses WordPress Application Passwords. Generate one in the user profile and place it in the .env file.
Q: Can I manage more than one site?
A: Yes. Define numbered environment variables (WORDPRESS_1_URL, WORDPRESS_2_URL, …). Each site gets a unique site_id that you can pass to any tool.
Q: Why do some SEO meta fields not persist after an update?
A: WordPress only stores meta keys that are registered with show_in_rest: true. The server reports dropped keys as warnings. Install a small companion plugin that registers the desired keys to make them writable.
Q: Is the SQL query tool safe?
A: It only accepts read‑only SELECT‑type statements, rejects modifying queries, and requires the WordPress user to have manage_options capability. The endpoint is customizable via WORDPRESS_SQL_ENDPOINT.
Q: How can I stop the server from stripping Yoast fields?
A: Set MCP_WP_STRIP_FIELDS= (empty) in your .env file to disable trimming.
Q: What runtime does the server need? A: Node.js version 18+ and npm. No additional build steps are required when using npx.
This is a Model Context Protocol (MCP) server for WordPress, allowing you to interact with your WordPress site using natural language via an MCP-compatible client like Claude for Desktop. This server exposes various WordPress data and functionality as MCP tools.
claude_desktop_config.json.example file.claude_desktop_config.json file.This server provides tools to interact with core WordPress data and supports multi-site management - manage multiple WordPress sites from a single MCP server instance.
Manage multiple WordPress sites from a single MCP server:
list_sites: List all configured WordPress sitesget_site: Get details about a specific site configurationtest_site: Test connection to a specific WordPress siteAll content and taxonomy tools support an optional site_id parameter to target specific sites.
Handles ALL content types (posts, pages, custom post types) with a single set of intelligent tools:
list_content: List any content type with filtering and paginationget_content: Get specific content by ID and typecreate_content: Create new content of any typeupdate_content: Update existing content of any type, including targeted partial editsdelete_content: Delete content of any typediscover_content_types: Find all available content types on your sitefind_content_by_url: Smart URL resolver that can find and optionally update content from any WordPress URL, including targeted partial editsget_content_by_slug: Search by slug across all content typesget_content_summary: Return a minimal summary (id, title, slug, status, excerpt, taxonomies, word count, Yoast SEO fields) for audit and lookup workflows. Look up by id or url.Handles ALL taxonomies (categories, tags, custom taxonomies) with a single set of tools:
discover_taxonomies: Find all available taxonomies on your sitelist_terms: List terms in any taxonomyget_term: Get specific term by IDcreate_term: Create new term in any taxonomyupdate_term: Update existing termdelete_term: Delete term from any taxonomyassign_terms_to_content: Assign terms to any content typeget_content_terms: Get all terms for any contentlist_media: List all media items (supports pagination and searching).get_media: Retrieve a specific media item by ID.create_media: Create a new media item from a URL or local file path.update_media: Update an existing media item.delete_media: Delete a media item.edit_media: Legacy alias for update_media kept for backward compatibility.list_users: List all users with filtering, sorting, and pagination options.get_user: Retrieve a specific user by ID.create_user: Create a new user.update_user: Update an existing user.delete_user: Delete a user.list_comments: List all comments with filtering, sorting, and pagination options.get_comment: Retrieve a specific comment by ID.create_comment: Create a new comment.update_comment: Update an existing comment.delete_comment: Delete a comment.list_plugins: List all plugins installed on the site.get_plugin: Retrieve details about a specific plugin.activate_plugin: Activate a plugin.deactivate_plugin: Deactivate a plugin.create_plugin: Create a new plugin.search_plugins: Search for plugins in the WordPress.org repository.get_plugin_info: Get detailed information about a plugin from the repository.execute_sql_query: Execute read-only SQL queries against the WordPress database (requires custom endpoint setup).Upload a local screenshot from the same machine running the MCP server:
{
"file_path": "./screenshots/homepage.png",
"title": "Homepage Screenshot",
"alt_text": "Homepage screenshot showing the hero section"
}
Upload media from a remote URL:
{
"source_url": "https://example.com/assets/hero-image.png",
"title": "Hero Image",
"caption": "Imported from the design system"
}
Use the returned media ID as featured media on new content:
{
"content_type": "post",
"title": "Release Notes",
"content": "<p>Launch summary...</p>",
"featured_media": 123
}
The find_content_by_url tool can:
/documentation/ → documentation custom post type)The get_content_summary tool returns a minimal, fixed-shape representation of a single piece of content. Designed for audit and lookup workflows where the full WP REST response — which can exceed 50KB on recipe posts because of the rendered Recipe Maker card HTML — is overkill.
Look up by ID (with optional content_type, defaulting to post):
{
"id": 4274,
"content_type": "post"
}
Look up by URL (content type is detected from the URL):
{
"url": "https://example.com/blog/easy-smoked-asparagus/"
}
id and url are mutually exclusive — provide exactly one.
The response shape is fixed:
{
"id": 4274,
"title": "Easy Smoked Asparagus & Hot Honey",
"slug": "easy-smoked-asparagus",
"status": "publish",
"link": "https://example.com/blog/easy-smoked-asparagus/",
"excerpt": "Smoky asparagus with hot honey.",
"date_modified": "2026-04-30T10:14:00",
"categories": [12, 7],
"tags": [33],
"featured_media": 9012,
"word_count": 875,
"yoast_focus_keyword": "smoked asparagus",
"yoast_meta_title": "Easy Smoked Asparagus | Example",
"yoast_meta_description": "Smoky charred asparagus finished with chili-lime hot honey."
}
Field notes:
title and excerpt are stripped to plain text (HTML tags removed, basic entities decoded).word_count prefers yoast_head_json.schema.@graph[].wordCount when Yoast SEO is active; otherwise it is computed from the rendered post content with HTML stripped.yoast_meta_title and yoast_meta_description are read from yoast_head_json on the post. They are null when Yoast SEO is not active.yoast_focus_keyword is read from meta._yoast_wpseo_focuskw. WordPress core only exposes meta keys that are registered with show_in_rest, and Yoast SEO does not register this key by default — so this field will typically be null unless a companion plugin registers it (see PR #17 for context on the broader meta-key REST exposure issue).yoast_head_json. The trim still applies to all other tools.All content operations use a single content_type parameter:
{
"content_type": "post", // for blog posts
"content_type": "page", // for static pages
"content_type": "product", // for WooCommerce products
"content_type": "documentation" // for custom post types
}
update_content and find_content_by_url.update_fields can patch the existing raw WordPress content without resending the full document.
To make exact matching easier, get_content and find_content_by_url both accept include_raw_content: true. When enabled, the response is fetched with WordPress edit context and includes a top-level content_raw field that matches what content_edit.target_text needs.
{
"content_type": "page",
"id": 7,
"include_raw_content": true
}
Append a short release note to the end of a post:
{
"content_type": "post",
"id": 42,
"content_edit": {
"operation": "append",
"value": "\n<p>Update: Early access is now open.</p>",
"content_format": "html"
}
}
Replace a unique HTML fragment or marker comment in place:
{
"content_type": "page",
"id": 7,
"content_edit": {
"operation": "replace",
"target_text": "<!-- pricing-card -->\n<p>Old price</p>\n<!-- /pricing-card -->",
"value": "<!-- pricing-card -->\n<p>New price</p>\n<!-- /pricing-card -->",
"content_format": "html"
}
}
Notes:
content.raw because entities may be escaped and markup may be expanded, so use include_raw_content when you need an exact target_text.target_text matches the stored raw WordPress content exactly.target_text appears multiple times, pass occurrence to choose the 1-based match.content_edit.convert_to_blocks when inserting Markdown or HTML that should become blocks.All taxonomy operations use a single taxonomy parameter:
{
"taxonomy": "category", // for categories
"taxonomy": "post_tag", // for tags
"taxonomy": "product_category", // for WooCommerce
"taxonomy": "skill" // for custom taxonomies
}
The taxonomy parameter accepts either the taxonomy slug or its rest_base
(they can differ for custom taxonomies, e.g. slug documentation_category
with rest_base documentation-categories). Tools resolve the identifier via
/wp/v2/taxonomies and error on unknown taxonomies instead of guessing.
assign_terms_to_content verifies the write against the WordPress response
and reports an error if the terms were not actually saved.
Sites running WP Recipe Maker (WPRM) store recipe cards in a separate wprm_recipe custom post type referenced by shortcode from the surrounding blog post. The unified content tools handle these recipes directly — no recipe-specific tool family is needed.
Reading recipes — get_content, list_content, find_content_by_url, and get_content_by_slug all work with content_type: "wprm_recipe". WPRM exposes the full structured recipe payload as a recipe field on the REST response, including ingredients, instructions, times, equipment, nutrition, notes, and rating.
Writing recipes — pass the recipe payload via custom_fields.recipe on create_content or update_content. WPRM hooks into the WordPress REST insert action (rest_insert_wprm_recipe) and reads recipe from the request body root, so any field documented by WPRM's data model is accepted.
The
recipepayload must be passed viacustom_fields(which spreads at the request body root). Themetaparameter nests its values under ametakey, which never reaches WPRM's REST hook.
Example update:
{
"content_type": "wprm_recipe",
"id": 4274,
"custom_fields": {
"recipe": {
"name": "Easy Smoked Asparagus",
"summary": "Smoky asparagus with hot honey.",
"servings": "4",
"servings_unit": "people",
"prep_time": "5",
"cook_time": "60",
"total_time": "65",
"ingredients": [
{
"name": "",
"ingredients": [
{ "uid": 0, "amount": "1", "unit": "Bunch", "name": "Asparagus Spears", "notes": "" },
{ "uid": 1, "amount": "1", "unit": "tbsp", "name": "Olive Oil", "notes": "" }
]
}
],
"instructions": [
{
"name": "",
"instructions": [
{ "uid": 0, "name": "", "text": "Preheat smoker to 225°F.", "ingredients": [] },
{ "uid": 1, "name": "", "text": "Drizzle with oil, season, smoke 1 hour.", "ingredients": [] }
]
}
],
"notes": "Thicker spears need more time."
}
}
}
Grouped ingredients and instructions — recipes can split items into named groups like "For the sauce" / "For the chicken". Each entry in the outer ingredients (or instructions) array is one group with its own name and inner array:
{
"ingredients": [
{ "name": "For the sauce", "ingredients": [ /* items */ ] },
{ "name": "For the chicken", "ingredients": [ /* items */ ] }
]
}
Commonly used recipe fields:
| Field | Type | Notes |
|---|---|---|
name |
string | Recipe card title |
summary |
string | Short blurb (HTML allowed) |
servings |
string | e.g. "4" |
servings_unit |
string | e.g. "people", "servings" |
prep_time |
string | minutes, e.g. "15" |
cook_time |
string | minutes |
total_time |
string | minutes |
ingredients |
array of groups | nested structure shown above |
instructions |
array of groups | nested structure shown above |
notes |
string | HTML allowed |
equipment |
array | items shaped { id, name, notes, amount, uid } |
image_url |
string | upload-by-URL when no image_id is supplied |
Course, cuisine, and keyword are stored as WPRM taxonomies (wprm_course, wprm_cuisine, wprm_keyword). Manage them with the unified taxonomy tools (list_terms, create_term, …) and link them to a recipe with assign_terms_to_content.
WPRM auto-syncs recipe.summary back to the WordPress post_content field on save. If you want the post body and the recipe summary to differ, pass content explicitly alongside custom_fields.recipe.
For managing a single WordPress site, use the following environment variables:
WORDPRESS_API_URL=https://your-wordpress-site.com
WORDPRESS_USERNAME=wp_username
WORDPRESS_PASSWORD=wp_app_password
To manage multiple WordPress sites from a single MCP server, use numbered environment variables:
# Site 1 (Production)
WORDPRESS_1_URL=https://production-site.com
WORDPRESS_1_USERNAME=admin
WORDPRESS_1_PASSWORD=app_password_1
WORDPRESS_1_ID=production
WORDPRESS_1_DEFAULT=true
WORDPRESS_1_ALIASES=prod,main
# Site 2 (Staging)
WORDPRESS_2_URL=https://staging-site.com
WORDPRESS_2_USERNAME=admin
WORDPRESS_2_PASSWORD=app_password_2
WORDPRESS_2_ID=staging
WORDPRESS_2_ALIASES=stage,dev
# Site 3 (Development)
WORDPRESS_3_URL=https://dev-site.com
WORDPRESS_3_USERNAME=admin
WORDPRESS_3_PASSWORD=app_password_3
WORDPRESS_3_ID=development
Multi-Site Configuration Options:
WORDPRESS_N_URL: WordPress site URL (required)WORDPRESS_N_USERNAME: WordPress username (required)WORDPRESS_N_PASSWORD: WordPress application password (required)WORDPRESS_N_ID: Site identifier (optional, defaults to siteN)WORDPRESS_N_DEFAULT: Set to true to make this the default site (optional, first site is default)WORDPRESS_N_ALIASES: Comma-separated aliases for site detection (optional)The server supports up to 10 sites. When using multi-site configuration, all tools accept an optional site_id parameter to target specific sites.
You can run this MCP server directly using npx without installing it globally:
npx -y @instawp/mcp-wp
Make sure you have a .env file in your current directory with the following variables:
WORDPRESS_API_URL=https://your-wordpress-site.com
WORDPRESS_USERNAME=wp_username
WORDPRESS_PASSWORD=wp_app_password
# Optional: Custom SQL query endpoint (default: /mcp/v1/query)
WORDPRESS_SQL_ENDPOINT=/mcp/v1/query
# Optional: Comma-separated list of top-level fields to strip from
# WordPress REST API responses before they are returned to the MCP
# client. Defaults to "yoast_head,yoast_head_json" — read-only schema
# markup that adds ~10KB to every response but is rarely useful to the
# LLM. Set to an empty string to disable trimming.
MCP_WP_STRIP_FIELDS=yoast_head,yoast_head_json
By default the server strips the top-level yoast_head and yoast_head_json
fields from every WordPress REST API response before returning it to the MCP
client. These fields contain Yoast SEO's pre-rendered schema markup, which the
LLM almost never needs but pays tokens for on every request.
MCP_WP_STRIP_FIELDS environment variable
(comma-separated). Set it to an empty string to disable trimming entirely.The meta parameter on create_content, update_content, and find_content_by_url (with update_fields.meta) forwards directly to the WordPress /wp/v2/{type}/{id} endpoint. WordPress core silently drops any meta key that has not been registered via register_post_meta(..., ['show_in_rest' => true]). The MCP server has no allowlist of its own — it relies on WordPress to enforce which keys persist.
This means SEO plugin keys are not writable through this MCP server by default, including:
_yoast_wpseo_* (focuskw, metadesc, title, opengraph-, twitter-, canonical, meta-robots-*, primary_category, …)rank_math_* (title, description, focus_keyword, robots, facebook_, twitter_, primary_category, …)wp_aioseo_posts), not wp_postmeta — not addressable via the meta field by any means.The server detects when WordPress dropped any keys you sent and prepends a Warning: block to the tool result listing them. This makes the silent drop visible to the LLM caller, but it cannot make WordPress accept the keys.
To enable SEO meta writes, install a small WordPress companion plugin that calls register_post_meta for each desired key with show_in_rest => true and an appropriate auth_callback. A separate mcp-wp-seo-bridge plugin is being scoped to do exactly this.
Plugin keys that the plugin author already registered for REST — for example Genesis layout meta (_genesis_layout), WP Recipe Maker fields (wprm-*), or ConvertKit's _wp_convertkit_post_meta. To check which keys round-trip on your site, write a test value via update_content and inspect the meta block in the response — if the key appears, it persisted.
The same limitation applies to term meta on unified-taxonomies tools (create_term, update_term).
The execute_sql_query tool allows you to run read-only SQL queries against your WordPress database. This is an optional feature that requires adding a custom REST API endpoint to your WordPress site.
Security Notes:
logs/wordpress-api.log - avoid including sensitive data in queriesmanage_options capability)Configuration: By default, the tool expects the endpoint at /mcp/v1/query. You can customize this by setting the WORDPRESS_SQL_ENDPOINT environment variable (e.g., WORDPRESS_SQL_ENDPOINT=/custom/v1/query).
To enable this feature, add the following code to your WordPress site (via a custom plugin or your theme's functions.php):
add_action('rest_api_init', function() {
register_rest_route('mcp/v1', '/query', array(
'methods' => 'POST',
'callback' => function($request) {
global $wpdb;
$query = $request->get_param('query');
// Additional security check
if (!current_user_can('manage_options')) {
return new WP_Error('unauthorized', 'Unauthorized', array('status' => 401));
}
// Only allow SELECT queries
if (stripos(trim($query), 'SELECT') !== 0) {
return new WP_Error('invalid_query', 'Only SELECT queries allowed', array('status' => 400));
}
$results = $wpdb->get_results($query, ARRAY_A);
if ($wpdb->last_error) {
return new WP_Error('query_error', $wpdb->last_error, array('status' => 400));
}
return array(
'results' => $results,
'num_rows' => count($results)
);
},
'permission_callback' => function() {
return current_user_can('manage_options');
}
));
});
After adding this code, you can use the execute_sql_query tool to run queries like:
SELECT * FROM wp_posts WHERE post_type = 'post' AND post_status = 'publish' LIMIT 10
Clone the Repository:
git clone <repository_url>
cd wordpress-mcp-server
Install Dependencies:
npm install
Create a .env file:
Create a .env file in the root of your project directory and add your WordPress API credentials.
For a single site:
WORDPRESS_API_URL=https://your-wordpress-site.com
WORDPRESS_USERNAME=wp_username
WORDPRESS_PASSWORD=wp_app_password
For multiple sites:
WORDPRESS_1_URL=https://site1.com
WORDPRESS_1_USERNAME=admin
WORDPRESS_1_PASSWORD=app_password_1
WORDPRESS_1_ID=site1
WORDPRESS_1_DEFAULT=true
WORDPRESS_2_URL=https://site2.com
WORDPRESS_2_USERNAME=admin
WORDPRESS_2_PASSWORD=app_password_2
WORDPRESS_2_ID=site2
Replace the placeholders with your actual values.
Build the Server:
npm run build
Configure Claude Desktop:
claude_desktop_config.json file.mcpServers section. You will need to provide the absolute path to the build/server.js file and your WordPress environment variables.Once you've configured Claude Desktop, the server should start automatically whenever Claude Desktop starts.
You can also run the server directly from the command line for testing:
npm start
or in development mode:
npm run dev
The repo uses Vitest for unit tests. Tests live under tests/ and cover the
multi-site SiteManager and the MCP tool registry wiring.
npm test # one-shot run
npm run test:watch # watch mode
Tests run on pull_request and on pushes to main via .github/workflows/test.yml.
The server uses a unified tool architecture to reduce complexity:
src/
├── server.ts # MCP server entry point
├── wordpress.ts # WordPress REST API client
├── cli.ts # CLI interface
├── config/
│ └── site-manager.ts # Multi-site management
├── types/
│ └── wordpress-types.ts # TypeScript definitions
└── tools/
├── index.ts # Tool aggregation
├── site-management.ts # Site management (3 tools)
├── unified-content.ts # Universal content management (8 tools)
├── unified-taxonomies.ts # Universal taxonomy management (8 tools)
├── media.ts # Media management (5 canonical tools + edit_media alias)
├── users.ts # User management (~5 tools)
├── comments.ts # Comment management (~5 tools)
├── plugins.ts # Plugin management (~5 tools)
├── plugin-repository.ts # WordPress.org plugin search (~2 tools)
└── sql-query.ts # Database queries (1 tool)
npm install.env file with your WordPress credentialsnpm run buildFeel free to open issues or make pull requests to improve this project. Check out CLAUDE.md for detailed development guidelines.
Please log in to share your review and rating for this MCP.
Explore related MCPs that share similar capabilities and solve comparable challenges
by modelcontextprotocol
A Model Context Protocol server for Git repository interaction and automation.
by zed-industries
A high‑performance, multiplayer code editor designed for speed and collaboration.
by modelcontextprotocol
Model Context Protocol Servers
by modelcontextprotocol
A Model Context Protocol server that provides time and timezone conversion capabilities.
by cline
An autonomous coding assistant that can create and edit files, execute terminal commands, and interact with a browser directly from your IDE, operating step‑by‑step with explicit user permission.
by upstash
Provides up-to-date, version‑specific library documentation and code examples directly inside LLM prompts, eliminating outdated information and hallucinated APIs.
by daytonaio
Provides a secure, elastic infrastructure that creates isolated sandboxes for running AI‑generated code with sub‑90 ms startup, unlimited persistence, and OCI/Docker compatibility.
by continuedev
Enables faster shipping of code by integrating continuous AI agents across IDEs, terminals, and CI pipelines, offering chat, edit, autocomplete, and customizable agent workflows.
by github
Connects AI tools directly to GitHub, enabling natural‑language interactions for repository browsing, issue and pull‑request management, CI/CD monitoring, code‑security analysis, and team collaboration.
{
"mcpServers": {
"wordpress-mcp": {
"command": "npx",
"args": [
"-y",
"@instawp/mcp-wp"
],
"env": {
"WORDPRESS_API_URL": "<YOUR_WORDPRESS_API_URL>",
"WORDPRESS_USERNAME": "<YOUR_WORDPRESS_USERNAME>",
"WORDPRESS_PASSWORD": "<YOUR_WORDPRESS_APPLICATION_PASSWORD>"
}
}
}
}claude mcp add wordpress-mcp npx -y @instawp/mcp-wp