MCP Serversshim-mcp
WordPress plugin · MCP server · GPL-2.0-or-later

WP-CLI is the transport, not a tool.

Shim MCP turns a WordPress site into an MCP server. Point your client at wp shim-mcp serve --user=admin and JSON-RPC moves over stdin and stdout: no port bound, no application password issued, no HTTP request made. Authenticated Streamable HTTP is there too, off the same server object, with the same permission chain.

56 abilities·13 domains·3 meta-tools·0 dependencies·0 telemetry endpoints·1 required binary (wp)
v1.0.0 tagged · releases · submitted to WordPress.org, awaiting review
wp shim-mcp serve --user=admin
→ stdin
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"claude-code"}}}
← stdout
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"Shim MCP"}}}
→ stdin
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
← stdout
{"jsonrpc":"2.0","id":2,"result":{"tools":[
  {"name":"shim-mcp-discover-abilities"},
  {"name":"shim-mcp-get-ability-info"},
  {"name":"shim-mcp-execute-ability"}
]}}  // three tools. not fifty-six.
→ stdin
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{
  "name":"shim-mcp-execute-ability",
  "arguments":{"ability_name":"shim-mcp/posts-list",
  "parameters":{"status":"draft"}}}}  // abilities keep the slash
← stdout
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text",
  "text":"{\"posts\":[{\"id\":214,\"title\":\"Draft: homelab notes\"}]}"}]}}
// gated by edit_posts, then read_post on every row.
a session, abridged · stepped by hand
Your client sees three tools, not fifty-six. shim-mcp-discover-abilities, shim-mcp-get-ability-info and shim-mcp-execute-ability are the entire wire-level tool surface. The 56 WordPress abilities are discovered at runtime and invoked through execute-ability, so your tool list stays small while the ability surface stays large. Mind the punctuation: abilities are named with a slash (shim-mcp/posts-list), but an MCP tool name cannot contain one, so the adapter rewrites it to a hyphen.
The pipe

Two transports. One server object. One permission chain.

Most of the work in connecting an AI client to WordPress is authentication plumbing. On a site you already have checked out locally, none of it needs to exist.

Two ways to reach WordPress from an MCP client The usual shape routes an MCP client through an HTTP POST, an application password and a REST route on port 443 before reaching WordPress, binding a port, issuing a credential and putting a request on the wire. Shim MCP over stdio connects the MCP client directly to a local wp shim-mcp serve process through a stdin and stdout pipe, with WordPress loaded in-process: zero ports bound, zero credentials issued and zero HTTP requests. THE USUAL SHAPE MCP client HTTP POST App password REST route :443 WordPress request on the wire credential issued port bound SHIM MCP · STDIO MCP client STDIN → ← STDOUT STDERR (diagnostics) wp shim-mcp serve WordPress loaded in-process
scroll the diagram sideways →The stdio path removes the authentication surface rather than securing it.
0 ports bound0 credentials issued0 HTTP requests
Transport 1 · stdio

A pipe, not a connection

wp shim-mcp serve reads newline-delimited JSON-RPC from STDIN, writes responses to STDOUT and diagnostics to STDERR. Identity comes from --user, which calls wp_set_current_user() before the loop starts. No REST route, no listening port, no token. The trust boundary is shell access to the box.

Transport 2 · Streamable HTTP

One route, for sites you aren't sat in front of

A single REST route at /wp-json/mcp/shim-mcp accepting POST (including batch), GET and DELETE, with session termination via the Mcp-Session-Id header. Authentication is WordPress Application Passwords over HTTP Basic. Both transports negotiate MCP 2025-06-18, 2025-03-26 and 2024-11-05.

Not implemented

SSE returns 405

handle_sse_request() returns HTTP 405 with an empty body, marked not yet implemented in the source. Streamable HTTP is the supported path. Worth saying out loud, because the bundled readme.txt still advertises SSE and it does not work.

Always pass --user. wp shim-mcp serve with no --user runs with no current user: is_user_logged_in() is false and every ability's permission check fails. Pass an ID, login or email: --user=admin.
The surface

56 abilities across 13 domains, registered through WordPress's own Abilities API.

Nothing lives in a private tool registry. Every ability goes through wp_register_ability(), declares the capability that gates it, and is grouped here by the admin screen you already know it from.

DomainAbilitiesCapabilityPer-object check
Posts
6 abilities
  • posts-list
  • posts-get
  • posts-create
  • posts-update
  • posts-delete
  • posts-replace-text
edit_posts · publish_posts · delete_posts read_post, edit_post, delete_post · edit_others_posts to reassign an author
Pages
6 abilities
  • pages-list
  • pages-get
  • pages-create
  • pages-update
  • pages-delete
  • pages-replace-text
edit_pages · publish_pages · delete_pages read_post, edit_post, delete_post · wp_kses_post unless unfiltered_html
TaxonomyPosts › Categories & Tags
4 abilities
  • taxonomy-list-categories
  • taxonomy-list-tags
  • taxonomy-create-term
  • taxonomy-update-term
edit_posts · manage_categories the taxonomy's own edit_terms capability
Searchacross post types
1 ability
  • content-search
read — the lowest gate in the set read_post on every hit, so an unprivileged caller sees nothing new
RevisionsPosts › Revisions
2 abilities
  • revisions-list
  • revisions-restore
edit_posts edit_post on the revision's parent post
Media
5 abilities
  • media-list
  • media-get
  • media-upload
  • media-update
  • media-delete
upload_files · delete_posts read_post, edit_post, delete_post · uploads validated against allowed file types
Users
6 abilities
  • users-list
  • users-get
  • users-list-roles
  • users-create
  • users-update
  • users-delete
list_users · create_users · edit_users · delete_users edit_user, delete_user · promote_users guard refuses any role carrying a capability you lack
Plugins
4 abilities
  • plugins-list
  • plugins-activate
  • plugins-deactivate
  • plugins-delete
activate_plugins · install_plugins · delete_plugins activate_plugin, deactivate_plugin · install and delete are removed by WordPress under DISALLOW_FILE_MODS; activate and deactivate are not
MenusAppearance › Menus
7 abilities
  • menus-list
  • menus-create
  • menus-list-items
  • menus-add-item
  • menus-update-item
  • menus-delete-item
  • menus-assign-location
edit_theme_options read_post when pointing an item at content, edit_post on update, delete_post on removal
WidgetsAppearance › Widgets
3 abilities
  • widgets-list-sidebars
  • widgets-list-types
  • widgets-list-in-sidebar
edit_theme_options read-only domain — no add, move, configure or remove
Comments
6 abilities
  • comments-list
  • comments-get
  • comments-create
  • comments-set-status
  • comments-update
  • comments-delete
moderate_comments read_post before reading, edit_comment before mutating, edit_user to attribute to another account
OptionsSettings
3 abilities
  • options-get
  • options-search
  • options-update
manage_options options-update refuses a 14-name blocklist, plus anything ending in user_roles
SystemTools
3 abilities
  • system-environment
  • system-read-debug-log
  • system-set-debug-constants
manage_options refused under DISALLOW_FILE_EDIT / DISALLOW_FILE_MODS, and by two pre-write sanity checks on the rewritten wp-config.php (opening tag intact, not shrunk below 80%)
The filter

Registered through WordPress, never a private registry.

includes/Abilities/Registry.php
// runs before every ability is stored
add_filter( 'wp_register_ability_args',
    [ self::class, 'expose_all_abilities' ], 10, 2 );

public static function expose_all_abilities(
    array $args, string $ability_name
): array {
    $args['meta']['mcp']['public'] = true;

    if ( ! isset( $args['meta']['mcp']['type'] ) ) {
        $args['meta']['mcp']['type'] = 'tool';
    }

    return $args;
}
And the cost of that, stated plainly. Because expose_all_abilities() hooks the wp_register_ability_args filter, every ability registered on the site becomes MCP-visible: including ones registered by other plugins. There is no allowlist and no admin toggle. That is the trade-off in both directions: abilities from other plugins work over MCP with zero adapter code, and you do not get to choose which.
The check

Holding edit_posts is not permission to edit any post.

Four links, in this order, on both transports. No code path in the plugin elevates, switches user, or short-circuits current_user_can().

The options blocklist

options-update refuses 14 names outright: siteurl, home, admin_email, new_admin_email, users_can_register, default_role, active_plugins, active_sitewide_plugins, recently_activated, template, stylesheet, db_version, initial_db_version, cron — plus anything ending in user_roles.

The promote_users guard

users-create and users-update refuse to grant any role carrying a capability the caller does not itself hold. You cannot change your own role, and you cannot delete yourself.

Install-from-URL is fenced

Installing plugins from an external source is not supported at all. The plugin abilities act only on what is already installed — list, activate, deactivate and delete — so no code is ever fetched and executed on your behalf.

These guard against a misbehaving client, not against a privileged caller — the caller already holds manage_options. That distinction is SECURITY.md's, not marketing's.

Up and running

Clone it, activate it, point a client at it.

One plugin. No Composer, no npm, no companion plugin, no account anywhere.

Install method
# 1 · clone into your plugins directory
$ git clone https://github.com/justadityaraj/shim-mcp.git \
    wp-content/plugins/shim-mcp

# 2 · activate
$ wp plugin activate shim-mcp

# 3 · register the server (stdio, no token, no port)
$ claude mcp add shim -- \
    wp shim-mcp serve --user=admin --path=/full/path/to/wordpress

--path matters because Claude Code will not be running from your WordPress root. --user is not optional.

// .cursor/mcp.json
{
  "mcpServers": {
    "shim-mcp": {
      "command": "wp",
      "args": [
        "--path=/full/path/to/wordpress",
        "shim-mcp", "serve", "--user=admin"
      ]
    }
  }
}

Same stdio process, declared instead of registered. The wp binary has to be on Cursor's PATH.

# For a site you are not sat in front of.
# Tools › Shim MCP › Generate mints an application password,
# then copy the config snippet the dashboard shows you.

$ curl -sX POST https://your-site.com/wp-json/mcp/shim-mcp \
    -u 'user:xxxx xxxx xxxx xxxx xxxx xxxx' \
    -H 'Content-Type: application/json' \
    -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
         "params":{"protocolVersion":"2025-06-18",
                   "capabilities":{},"clientInfo":{"name":"curl"}}}'

Serve it over HTTPS: application passwords travel as HTTP Basic credentials. The one-click generator mints the password for your account, so a least-privilege credential has to be created by hand from a lower-privileged user's profile.

Prefer a zip? v1.0.0 is tagged — download it and use Plugins → Add New → Upload Plugin.

The edges

What it doesn't do.

Everything here is checkable against the source in about a minute. Better you read it from me than find it yourself.

No SSE

handle_sse_request() returns 405 with an empty body. Streamable HTTP over POST / GET / DELETE is the supported path, and it is the one the README documents.

Widgets are read-only, end to end

You can list sidebars, widget types and what sits in a given sidebar. You cannot add, move, configure or remove a widget. Three abilities, all reads.

No CI and no test suite in the repo

Verification is done by hand: Plugin Check, PHP_CodeSniffer, PHPStan and a run against a live site. Nothing runs automatically on a commit. The plugin is feature-complete and the abilities are documented, but it has not yet been exercised across a broad range of hosting environments.

Installing for stdio still registers the HTTP route

/wp-json/mcp/shim-mcp comes up with the plugin whether or not you use it. It can be disabled through the mcp_adapter_create_default_server and mcp_adapter_default_server_config filters — PHP, not a setting.

The annotation hints aren't uniformly consistent

posts-replace-text is flagged destructive; pages-replace-text is not. If your client keys its confirmation prompts off MCP annotations, don't lean on them here.

Roughly half the PHP is not mine

The protocol layer under includes/Server/ derives from the WordPress MCP Adapter; abilities register through the Abilities API that WordPress ships in core from 6.9. Broken down in CREDITS.md. The 56 abilities, the dashboard, the WP-CLI stdio bridge and the packaging are original.

Not translation-ready yet

Strings are wrapped and the text domain is declared, but load_plugin_textdomain() is never called and there is no /languages directory.

Who built this

Aditya Raj Singh
Aditya Raj Singh
builder · automation & MCP servers · homelab tinkerer

I keep wiring Claude into the tools I actually run. WordPress was the awkward one: every option meant a token, a port and an HTTP round trip to a site already sitting on my own disk. So the plugin makes WP-CLI the transport and gets out of the way. More of what I build over at adityarajsingh.com.

Questions

WordPress MCP, answered.

What is Shim MCP?
An open-source WordPress plugin that turns a WordPress site into a Model Context Protocol (MCP) server. It registers 56 abilities across 13 domains through WordPress's own Abilities API and exposes them to clients like Claude Code, Claude Desktop and Cursor over WP-CLI stdio or authenticated Streamable HTTP. A shim is a thin layer between two interfaces that changes neither: WordPress speaks the Abilities API, AI clients speak MCP, and this translates between them and does nothing else.
Does it need a companion plugin or a cloud service?
No. The MCP server and the 56 abilities ship in the one plugin. No composer.json, no vendor directory, no npm install. There is no relay, no vendor account and no telemetry: only one outbound HTTP call exists anywhere in the codebase: the Test Connection probe against the site's own REST route.
Will I see 56 tools in my MCP client?
No. tools/list returns exactly three: shim-mcp-discover-abilities, shim-mcp-get-ability-info and shim-mcp-execute-ability — an MCP tool name cannot contain a slash, so the adapter rewrites the ability prefix to a hyphen. The 56 abilities are discovered at runtime and invoked through execute-ability, so your tool list stays small while the ability surface stays large.
Does the stdio transport open a port or need an application password?
No. The server runs as a local process: JSON-RPC on STDIN, responses on STDOUT, diagnostics on STDERR. No port bound, no application password issued, no HTTP request made. One caveat worth knowing: installing the plugin still registers the HTTP route at /wp-json/mcp/shim-mcp, and that route can only be turned off through the mcp_adapter_* PHP filters, not a setting.
What does --user actually do, and is it required?
It sets the WordPress user the stdio session acts as, by calling wp_set_current_user() before the loop starts. It is effectively mandatory: without it no current user is set, is_user_logged_in() is false, and every ability's permission check fails. Pass an ID, login or email.
Is stdio more secure than HTTP?
More accurately, it removes the authentication surface rather than securing it. There is no authentication on the stdio path at all: identity is whatever --user names, so the trust boundary is shell access to the machine. Authorization is identical on both transports, because both run off the same server object and the same permission chain: a client cannot get a weaker check by picking a transport.
Does it support SSE?
No. The SSE request handler returns HTTP 405 with an empty body and is marked not yet implemented in the source. The supported HTTP path is Streamable HTTP over POST, GET and DELETE against a single REST route.
How do I install it?
Clone into wp-content/plugins/shim-mcp, run wp plugin activate shim-mcp, then register it: claude mcp add shim -- wp shim-mcp serve --user=admin --path=/full/path/to/wordpress. Version 1.0.0 is tagged, so downloading the zip and uploading it through Plugins works too. Full commands are in the install section above.
How much of it is original code?
Roughly half the PHP by line count. The 56 abilities, the admin dashboard, the WP-CLI stdio bridge and the packaging are original. The MCP protocol layer under includes/Server/ derives from the WordPress MCP Adapter (GPL-2.0); abilities register through the Abilities API that WordPress ships in core. Broken down in CREDITS.md.
Is it on WordPress.org?
Submitted and awaiting review. It is not approved or listed yet, so install it from the tagged release or from source in the meantime. The plugin is GPL-2.0-or-later per its plugin header. Requirements are WordPress 6.9+ and PHP 8.0+, plus WP-CLI for the stdio transport only.
$ wp shim-mcp serve --user=admin