MW
Home Blog Which tools and resources should a WordPress MCP server expose?

Which tools and resources should a WordPress MCP server expose?

Tool granularity, intent-shaping, default surface area, and which WordPress resources matter for retrieval. A starter schema you can copy.

Mahesh Waghmare
Mahesh Waghmare
7 min read
Share:
This is a comprehensive guide based on real-world experience and best practices from production projects.

WHICH TOOLS AND RESOURCES SHOULD A…

Advertisement

This is the tools-and-resources spoke of the WordPress + MCP cluster. The setup walkthrough showed you how to expose tools; the auth spoke showed you how to secure them. This page is about what to expose in the first place — the API-design question that most WordPress MCP servers get wrong.

The framing question: what intent does each tool serve? Not “what REST endpoint does it wrap.” If your answer is “this tool lets the model send arbitrary JSON to PUT /wp/v2/posts/123,” you’ve built an API proxy, not an MCP server. The model is no more useful than curl. The point of MCP is to give an LLM well-named, narrow-purpose tools that map to things the user actually wants to do.

Tool granularity: the rule of one verb

Aim for one intent per tool. The shape: noun_verb (post_publish, post_unpublish, comment_approve) or noun_attribute_set (post_featured_image_set, post_excerpt_set). The LLM should be able to read the tool name and know — without reading the description — what it does and roughly what arguments it takes.

A counter-example that’s tempting and wrong:

{
  name: 'post_update',
  description: 'Update any field on a post. Pass the field name and new value.',
  inputSchema: { /* huge union of every settable field */ }
}

This is “the WordPress REST API as an MCP tool.” Every WP plugin author writes this first. It’s bad for three reasons.

The LLM has to reason about which field name to pass, often guessing. The user, looking at the approval dialog, sees { field: "status", value: "draft" } and has to mentally decode what that does. And the server has no way to apply different friction levels to different field changes — setting a tag is now indistinguishable from unpublishing.

Replace it with intent-shaped tools:

{ name: 'post_publish',          inputSchema: { post_id: number } }
{ name: 'post_unpublish',        inputSchema: { post_id: number } }
{ name: 'post_move_to_trash',    inputSchema: { post_id: number, i_understand_this_is_destructive: true } }
{ name: 'post_set_title',        inputSchema: { post_id: number, title: string } }
{ name: 'post_set_excerpt',      inputSchema: { post_id: number, excerpt: string } }
{ name: 'post_add_tag',          inputSchema: { post_id: number, tag: string } }
{ name: 'post_remove_tag',       inputSchema: { post_id: number, tag: string } }
{ name: 'post_set_featured_image', inputSchema: { post_id: number, media_id: number } }

More tools, smaller blast radius each, clearer audit log, easier for the user to approve.

The default surface area

Most WordPress MCP servers should ship the following tools by default. Anything beyond this should be opt-in.

For discovery and read: post_list, post_get, post_search, media_search, taxonomy_list, comment_list_pending. These are uniformly safe — the worst they can do is leak data the user has access to anyway, which the auth layer already controls. They should rarely need explicit approval after first use.

For content authoring (write but reversible): post_create_draft, post_set_title, post_set_content, post_set_excerpt, post_add_tag, post_remove_tag, post_set_featured_image. Drafts are reversible; tags can be removed; titles can be edited again. Approval per session is enough.

For publishing (intent-bearing, partially reversible): post_publish, post_unpublish, post_schedule. These change what visitors see. Approve per call.

For destructive (irreversible from the agent’s perspective): post_move_to_trash, media_delete, comment_trash. Require the i_understand_this_is_destructive: true parameter, log every call, and assume the user will read the audit log before they sleep.

What to not ship by default, even if you’re tempted: anything that touches user accounts (user_create, user_set_role), anything that touches site settings (option_set, theme_activate), anything that runs arbitrary code (plugin_install, update_run). These belong in a separate admin_* tool family, gated behind explicit per-conversation enablement.

Resources, briefly

MCP also lets you expose resources — read-only blobs the client can fetch directly without calling a tool. The clearest fit for WordPress:

A single resource that returns the site map (site://structure) — an outline of pages, categories, tags, and primary navigation. This gives the model topological context without forcing it to call post_list and walk the result. Cache it; serve it cheaply.

A resource per published post (post://{slug}) — the rendered content, plus metadata. Lets the model retrieve a specific post by name without needing to know the ID. Pairs well with the search tool: post_search returns slugs, the model reads the resource.

A resource for author profiles (author://{username}) if your site has multiple contributors. Helps the model attribute content correctly.

What you should not expose as resources: anything sensitive (drafts, private posts, internal comments), anything that requires fresh data each call (analytics, sales numbers), anything large enough to blow the context window (the entire post archive).

A starter schema

If you want to ship today, here’s a working starter — eight tools, two resources, scoped for a Editor-equivalent role:

const TOOLS = [
  { name: 'post_search',          args: ['query', 'limit?'] },
  { name: 'post_get',             args: ['post_id'] },
  { name: 'post_list_drafts',     args: ['limit?', 'author?'] },
  { name: 'post_create_draft',    args: ['title', 'content', 'tags?'] },
  { name: 'post_set_title',       args: ['post_id', 'title'] },
  { name: 'post_set_content',     args: ['post_id', 'content'] },
  { name: 'post_publish',         args: ['post_id'] },
  { name: 'post_unpublish',       args: ['post_id'] },
];

const RESOURCES = [
  { uri: 'site://structure', mime: 'application/json' },
  { uri: 'post://{slug}',    mime: 'text/html' },
];

That’s enough to give an agent meaningful authoring superpowers, and small enough that you can audit every call category in one head-pass. Grow from there based on what your users actually ask for — not what the REST API happens to support.

Closing the loop

If you’ve read all four pages of the cluster — the pillar, the setup guide, the auth spoke, and this one — you have a complete starting point for a production-grade WordPress MCP server.

What I’d build next, if I were continuing past this cluster: a small npm package that ships these eight tools, a configurable role-to-tool mapping, audit logging out of the box, and a wp-cli adapter so the same surface works against any WordPress site without writing custom REST calls. That’s the seed of a real plugin. If you ship something in this direction, I want to read about it.

Get weekly notes in your inbox

Practical tips, tutorials and resources. No spam.