Skip to main content
Glama
README.md
# MCP Site Manager

> Manage WordPress from Claude, ChatGPT, Cursor and any other MCP client.

[![License: GPL v2+](https://img.shields.io/badge/License-GPL_v2%2B-blue.svg)](https://www.gnu.org/licenses/gpl-2.0)
[![PHP 8.0+](https://img.shields.io/badge/PHP-8.0%2B-777bb4.svg)](https://www.php.net/releases/8.0/)
[![WordPress 6.8+](https://img.shields.io/badge/WordPress-6.8%2B-21759b.svg)](https://wordpress.org/download/)

MCP Site Manager exposes ~74 WordPress capabilities — content, taxonomies, media, comments, users, plugins, themes, options, navigation menus, diagnostics, cache and cron — as [Model Context Protocol](https://modelcontextprotocol.io/) tools. It plugs into the [WP MCP Adapter](https://github.com/WordPress/mcp-adapter) library (already bundled with WooCommerce 10.3+) and any MCP-compatible AI agent — Claude Desktop, Claude Code, Cursor, VS Code, ChatGPT — can drive your WordPress site.

> šŸ“– **New to the WordPress MCP Adapter?** Read the official primer on the WordPress Developer Blog: [*From Abilities to AI Agents: Introducing the WordPress MCP Adapter*](https://developer.wordpress.org/news/2026/02/from-abilities-to-ai-agents-introducing-the-wordpress-mcp-adapter/). It explains the Abilities API, the adapter's transports, and how clients connect.

## Quick start

1. Install and activate MCP Site Manager.
2. **If the MCP Adapter library is missing**, an admin notice appears with a one-click *Install MCP Adapter* button that downloads the latest release from [WordPress/mcp-adapter Releases](https://github.com/WordPress/mcp-adapter/releases). Skipped on sites that already ship the library (e.g. WooCommerce).
3. Visit **Settings → MCP Site Manager** for your live connection URL and ready-to-paste client snippets.
4. Generate an Application Password from your user profile (HTTP transport) — or skip this step entirely if you'll use the STDIO transport for local dev.
5. Paste the snippet into your MCP client (examples below).

> **Pin a specific MCP Adapter release tag** by filtering `mcpsm_adapter_download_url` to a stable Releases URL.

## Transports

The MCP Adapter ships two transports. Pick by where the AI client and the WordPress install live relative to each other.

### HTTP — for remote / production sites

Best when WordPress is publicly reachable. The AI client speaks JSON-RPC over HTTPS through the [`@automattic/mcp-wordpress-remote`](https://www.npmjs.com/package/@automattic/mcp-wordpress-remote) proxy.

- **Auth**: WordPress Application Password (basic auth). The article also mentions custom OAuth / JWT for advanced setups; this plugin works with whatever the adapter accepts.
- **Production tip**: create a dedicated WP user with the minimum capabilities required for the abilities you expose. The AI's blast radius equals that user's caps.

### STDIO — for local development

Best when WordPress runs on the same machine as the AI client. The client spawns `wp mcp-adapter serve` directly via WP-CLI — **no Application Password, no HTTPS, nothing to expose**.

```bash
wp mcp-adapter serve --server=mcp-adapter-default-server --user=admin
```

The client invokes this command on its own; you don't run it manually.

## Connecting your AI client

> šŸ”Œ **See also**: [*Connecting AI applications*](https://developer.wordpress.org/news/2026/02/from-abilities-to-ai-agents-introducing-the-wordpress-mcp-adapter/#connecting-ai-applications) on the WordPress Developer Blog — the upstream walkthrough for Claude Desktop, Claude Code, Cursor and VS Code, plus STDIO vs HTTP transport guidance. The snippets below are pre-adapted for this plugin's default server endpoint.

Replace `https://your-site.com` and `your-username` / `your-password` (Application Password) with your own values. For STDIO, replace `/path/to/wordpress` with your wp-cli `--path`.

### Claude Desktop

Config at `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows). Open via **Settings → Developer → Edit config**.

<details>
<summary><strong>HTTP</strong> (remote site)</summary>

```json
{
  "mcpServers": {
    "mcp-site-manager": {
      "command": "npx",
      "args": ["-y", "@automattic/mcp-wordpress-remote@latest"],
      "env": {
        "WP_API_URL": "https://your-site.com/wp-json/mcp/mcp-adapter-default-server",
        "WP_API_USERNAME": "your-username",
        "WP_API_PASSWORD": "your application password"
      }
    }
  }
}
```
</details>

<details>
<summary><strong>STDIO</strong> (local dev)</summary>

```json
{
  "mcpServers": {
    "mcp-site-manager": {
      "command": "wp",
      "args": [
        "--path=/path/to/wordpress",
        "mcp-adapter",
        "serve",
        "--server=mcp-adapter-default-server",
        "--user=admin"
      ]
    }
  }
}
```
</details>

Quit and relaunch Claude Desktop. Try: *"List my latest 5 posts"*.

### Claude Code

Config at `~/.claude.json` (global) or `.mcp.json` (project). Same shape as Claude Desktop — use `mcpServers`. The HTTP and STDIO snippets above work verbatim.

### Cursor

Config at `~/.cursor/mcp.json` or via **Settings → Tools and MCP → Add Custom MCP**. Same `mcpServers` shape as Claude Desktop.

### VS Code

Config at `.vscode/mcp.json` in your project. **Key is `servers`, not `mcpServers`** — this is the only client that differs.

<details>
<summary><strong>HTTP</strong></summary>

```json
{
  "servers": {
    "mcp-site-manager": {
      "command": "npx",
      "args": ["-y", "@automattic/mcp-wordpress-remote@latest"],
      "env": {
        "WP_API_URL": "https://your-site.com/wp-json/mcp/mcp-adapter-default-server",
        "WP_API_USERNAME": "your-username",
        "WP_API_PASSWORD": "your application password"
      }
    }
  }
}
```
</details>

<details>
<summary><strong>STDIO</strong></summary>

```json
{
  "servers": {
    "mcp-site-manager": {
      "command": "wp",
      "args": [
        "--path=/path/to/wordpress",
        "mcp-adapter",
        "serve",
        "--server=mcp-adapter-default-server",
        "--user=admin"
      ]
    }
  }
}
```
</details>

### ChatGPT and other clients

Any client that speaks MCP over HTTP or STDIO works. Use the HTTP block above; check your client's docs for the equivalent of `mcpServers`/`servers`.

## Architecture

```
mcp-site-manager/
ā”œā”€ā”€ mcp-site-manager.php          # Plugin bootstrap: header, constants, hooks
ā”œā”€ā”€ composer.json                 # PSR-4 → Mrabbani\McpSiteManager\
ā”œā”€ā”€ readme.txt                    # wp.org listing
ā”œā”€ā”€ includes/
│   ā”œā”€ā”€ Plugin.php                # Singleton, dependency check, hook wiring
│   ā”œā”€ā”€ Admin/
│   │   ā”œā”€ā”€ SettingsPage.php      # Settings → MCP Site Manager UI
│   │   └── AbilityLog.php        # Custom DB table + recent invocations
│   ā”œā”€ā”€ Abilities/
│   │   ā”œā”€ā”€ AbilityBundle.php     # Abstract base — declarative ability map
│   │   ā”œā”€ā”€ Content/              # Posts, pages, custom post types
│   │   ā”œā”€ā”€ Taxonomy/
│   │   ā”œā”€ā”€ Media/
│   │   ā”œā”€ā”€ Comments/
│   │   ā”œā”€ā”€ Users/
│   │   ā”œā”€ā”€ Plugins/              # install/activate/update/delete
│   │   ā”œā”€ā”€ Themes/               # list/active/switch/install/update/delete
│   │   ā”œā”€ā”€ Options/              # allowlisted only
│   │   ā”œā”€ā”€ Menus/
│   │   ā”œā”€ā”€ Diagnostics/          # health overview, debug log tail
│   │   └── Maintenance/          # cache flush, cron list/run/unschedule
│   └── Support/
│       ā”œā”€ā”€ RestInvoker.php       # internal WP_REST_Request dispatcher
│       ā”œā”€ā”€ SchemaBuilder.php     # JSON Schema helpers for ability inputs
│       ā”œā”€ā”€ ErrorMapper.php       # WP_Error/Throwable → MCP envelope
│       ā”œā”€ā”€ AbilityRunner.php     # try/catch + log + error mapping
│       └── OptionsAllowlist.php
└── tests/
    ā”œā”€ā”€ Support/                  # PHPUnit unit tests
    └── Integration/              # wp-env + curl JSON-RPC smoke tests
```

### How it works

1. **Bootstrap.** On `plugins_loaded` priority 5, `Plugin::boot()` checks for the MCP Adapter dependency and wires hooks.
2. **Category registration.** On `wp_abilities_api_categories_init`, registers the `mcpsm` category.
3. **Ability registration.** On `wp_abilities_api_init`, each `AbilityBundle` registers its abilities via `wp_register_ability('mcpsm/<verb>', […])`. MCP Adapter's default server discovers registered abilities on its own — no filter wiring required. Clients see them as `mcpsm-<verb>` (slashes are mcp-adapter-rewritten to hyphens).
4. **Execution.** When a client calls `tools/call`, `AbilityBundle::register()` wraps the execute callback in `AbilityRunner::run()`, which centralizes try/catch, logging to the activity log table, and mapping `WP_Error`/`Throwable` to JSON-RPC error envelopes.

### Two ability patterns

- **REST-wrapping** (most abilities). The `execute_callback` builds a `WP_REST_Request` and dispatches via `rest_do_request()`. Reuses every existing REST permission check, sanitizer and schema validator. No reimplementation.
- **Direct PHP** (plugin/theme install/activate/delete, options, cache, cron, diagnostics). Where WP has no REST endpoint, the callback calls core APIs directly (`Plugin_Upgrader`, `switch_theme`, `wp_cache_flush`, etc.) with explicit `current_user_can()` checks.

### Permissions

Every ability runs with the **calling user's WordPress capabilities**. A subscriber can only do what a subscriber can do. Authentication uses WP core's [Application Passwords](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/) — no custom auth code. Direct-PHP abilities use granular WP caps:

| Domain | Required cap |
|---|---|
| Plugins (all verbs) | `activate_plugins`; install/update/delete also need `install_plugins`/`update_plugins`/`delete_plugins` |
| Themes (all verbs) | `switch_themes`; install/update/delete also need `install_themes`/`update_themes`/`delete_themes` |
| Options | `manage_options` |
| Diagnostics | `manage_options` |
| Maintenance (cache, cron) | `manage_options` |
| Comments moderation | `moderate_comments` |

REST-wrapping abilities defer to the wrapped endpoint's own permission callback (e.g. `edit_posts`, `upload_files`).

### Options allowlist

To prevent accidental destruction of plugin-internal options or auth data, the Options bundle only reads/writes a hard-coded allowlist:

```
blogname, blogdescription, permalink_structure, default_category,
posts_per_page, timezone_string, date_format, time_format,
start_of_week, WPLANG, default_comment_status, default_ping_status,
comment_registration, show_on_front, page_on_front, page_for_posts
```

Anything outside the list is rejected with a `403` and an `allowed_keys` payload telling the client what's available.

## Ability inventory (~74)

| Domain | Abilities |
|---|---|
| Posts | `posts-list`, `posts-get`, `posts-create`, `posts-update`, `posts-delete` |
| Pages | `pages-list`, `pages-get`, `pages-create`, `pages-update`, `pages-delete` |
| CPT | `cpt-list-types`, `cpt-list`, `cpt-get`, `cpt-create`, `cpt-update`, `cpt-delete` |
| Taxonomy | `taxonomies-list`, `terms-list`, `terms-get`, `terms-create`, `terms-update`, `terms-delete` |
| Media | `media-list`, `media-get`, `media-upload`, `media-update`, `media-delete` |
| Comments | `comments-list`, `comments-get`, `comments-create`, `comments-update`, `comments-delete`, `comments-moderate` |
| Users | `users-list`, `users-get`, `users-me`, `users-create`, `users-update`, `users-delete` |
| Plugins | `plugins-list`, `plugins-activate`, `plugins-deactivate`, `plugins-install`, `plugins-update`, `plugins-delete`, `plugins-search` |
| Themes | `themes-list`, `themes-active`, `themes-switch`, `themes-install`, `themes-update`, `themes-delete` |
| Options | `options-list`, `options-get`, `options-update` |
| Menus | `menus-list`, `menus-get`, `menus-create`, `menus-update`, `menus-delete`, `menu-items-{list,get,create,update,delete}`, `menu-locations-list` |
| Diagnostics | `health-overview`, `health-debug-log-tail`, `health-rest-status` |
| Maintenance | `cache-flush-rewrite`, `cache-flush-object`, `cron-list`, `cron-run`, `cron-unschedule` |

Tool names as seen by MCP clients are hyphen-prefixed: `mcpsm-posts-list`, `mcpsm-themes-active`, etc.

## Local development

### Prerequisites

- PHP ≄ 8.0
- Composer
- Node.js 18+ (for `@wordpress/env`)
- Docker Desktop (for wp-env)

### Setup

```bash
git clone https://github.com/mrabbani/mcp-site-manager.git
cd mcp-site-manager
composer install
npm install

# Place mcp-adapter alongside this repo (sibling directory) so .wp-env.json finds it:
git clone https://github.com/WordPress/mcp-adapter.git ../mcp-adapter
( cd ../mcp-adapter && composer install )

npx wp-env start
npx wp-env run cli wp rewrite structure '/%postname%/' --hard
npx wp-env run cli wp rewrite flush --hard
```

WordPress is now live at http://localhost:8890 (admin / password).

### Tests

```bash
# Unit tests (no WordPress)
./vendor/bin/phpunit --testsuite=unit

# Integration tests (against running wp-env)
APP_PW=$(npx wp-env run cli wp user application-password create admin "tests" --porcelain | grep -E '^[a-zA-Z0-9]{20,}' | head -1)

MCPSM_APP_PW="$APP_PW" \
MCPSM_URL="http://localhost:8890/wp-json/mcp/mcp-adapter-default-server" \
MCPSM_USER="admin" \
  ./vendor/bin/phpunit --testsuite=integration
```

Expected: `OK (9 tests, 70 assertions)` for integration; `OK (9 tests, 17 assertions)` for unit.

### Manual probe

```bash
APP_PW="<paste from above>"

# 1. Initialize the MCP session, capture session id
SID=$(curl -sS -i -u admin:$APP_PW \
  -X POST -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  http://localhost:8890/wp-json/mcp/mcp-adapter-default-server \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
  2>&1 | grep -i "Mcp-Session-Id:" | awk '{print $2}' | tr -d '\r')

# 2. Send the "initialized" notification (required by MCP)
curl -sS -u admin:$APP_PW \
  -X POST -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -H "Mcp-Session-Id: $SID" \
  http://localhost:8890/wp-json/mcp/mcp-adapter-default-server \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' > /dev/null

# 3. List tools
curl -sS -u admin:$APP_PW \
  -X POST -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -H "Mcp-Session-Id: $SID" \
  http://localhost:8890/wp-json/mcp/mcp-adapter-default-server \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | python3 -m json.tool | head -50

# 4. Call a tool
curl -sS -u admin:$APP_PW \
  -X POST -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -H "Mcp-Session-Id: $SID" \
  http://localhost:8890/wp-json/mcp/mcp-adapter-default-server \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"mcpsm-themes-active","arguments":{}}}' \
  | python3 -m json.tool
```

## Adding a new ability

Each ability is a single entry in a bundle's `abilities()` map. Example: a hypothetical `posts-trash-empty`:

```php
// In includes/Abilities/Content/PostsBundle.php

public function abilities(): array
{
    return [
        // ... existing abilities ...

        'posts-trash-empty' => [
            'label'               => __('Empty trash', 'mcp-site-manager'),
            'description'         => __('Permanently delete every trashed post.', 'mcp-site-manager'),
            'input_schema'        => S::object([]),
            'permission_callback' => self::require_cap('delete_posts'),
            'execute' => function () {
                $trashed = get_posts(['post_status' => 'trash', 'numberposts' => -1, 'fields' => 'ids']);
                $count = 0;
                foreach ($trashed as $id) {
                    if (wp_delete_post($id, true)) $count++;
                }
                return ['deleted' => $count];
            },
        ],
    ];
}
```

That's it. The bundle base wraps your `execute` in `AbilityRunner` (logging + error handling), registers it via `wp_register_ability()` with `meta.mcp.public = true`, and MCP Adapter's default server picks it up automatically as `mcpsm-posts-trash-empty` — no filter wiring required.

## Contributing

Issues and PRs welcome at https://github.com/mrabbani/mcp-site-manager/issues.

Local checks before submitting:

```bash
# Lint
find includes tests -type f -name "*.php" -print0 | xargs -0 -I{} php -l {} | grep -v "No syntax errors" || echo "ALL CLEAN"

# Tests
./vendor/bin/phpunit --testsuite=unit
# ...integration as above
```

## License

GPL-2.0-or-later. See [LICENSE](LICENSE) (or the GPL-2.0 text at https://www.gnu.org/licenses/gpl-2.0.html).

## Credits

Built by [mrabbani](https://profiles.wordpress.org/mrabbani/) on top of [WordPress's MCP Adapter](https://github.com/WordPress/mcp-adapter) and the [WordPress Abilities API](https://make.wordpress.org/core/?s=abilities+api) shipping in WordPress 6.9.