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.
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"claude-code"}}}
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"Shim MCP"}}}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"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.
{"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
{"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.
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.
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.
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.
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.
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.
--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.
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.
| Domain | Abilities | Capability | Per-object check |
|---|---|---|---|
| Posts | 6 abilities
|
edit_posts · publish_posts · delete_posts | read_post, edit_post, delete_post · edit_others_posts to reassign an author |
| Pages | 6 abilities
|
edit_pages · publish_pages · delete_pages | read_post, edit_post, delete_post · wp_kses_post unless unfiltered_html |
| TaxonomyPosts › Categories & Tags | 4 abilities
|
edit_posts · manage_categories | the taxonomy's own edit_terms capability |
| Searchacross post types | 1 ability
|
read — the lowest gate in the set | read_post on every hit, so an unprivileged caller sees nothing new |
| RevisionsPosts › Revisions | 2 abilities
|
edit_posts | edit_post on the revision's parent post |
| Media | 5 abilities
|
upload_files · delete_posts | read_post, edit_post, delete_post · uploads validated against allowed file types |
| Users | 6 abilities
|
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
|
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
|
edit_theme_options | read_post when pointing an item at content, edit_post on update, delete_post on removal |
| WidgetsAppearance › Widgets | 3 abilities
|
edit_theme_options | read-only domain — no add, move, configure or remove |
| Comments | 6 abilities
|
moderate_comments | read_post before reading, edit_comment before mutating, edit_user to attribute to another account |
| OptionsSettings | 3 abilities
|
manage_options | options-update refuses a 14-name blocklist, plus anything ending in user_roles |
| SystemTools | 3 abilities
|
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%) |
// 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; }
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.
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().
Over HTTP, the REST route's own permission_callback runs first and defaults to current_user_can('read'). Over stdio there is no gate here at all: the identity is whatever --user named.
execute-ability delegates, it does not decideIt requires a logged-in user, checks the ability is MCP-public, then hands off to the target ability's own check_permissions(). It never runs the callback on its own authority.
All 56 abilities declare a permission_callback wrapping current_user_can() with a specific capability: edit_posts, upload_files, moderate_comments, manage_options, and so on.
30 of the 56 abilities then re-check the object-specific capability — read_post, edit_post, delete_post, edit_user, delete_user, edit_comment, activate_plugin, deactivate_plugin — against that object before reading or mutating it. A contributor's client cannot edit an editor's post.
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.
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.
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.
One plugin. No Composer, no npm, no companion plugin, no account anywhere.
Prefer a zip? v1.0.0 is tagged — download it and use Plugins → Add New → Upload Plugin.
Everything here is checkable against the source in about a minute. Better you read it from me than find it yourself.
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.
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.
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.
/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.
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.
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.
Strings are wrapped and the text domain is declared, but load_plugin_textdomain() is never called and there is no /languages directory.
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.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./wp-json/mcp/shim-mcp, and that route can only be turned off through the mcp_adapter_* PHP filters, not a setting.--user actually do, and is it required?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.--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.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.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.