WP Manifestindependent plugin directory
manifest / ai / lean-mcp

Lean MCP

A small MCP server for WordPress

★ 0stars

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/abr4xas/lean-mcp/archive/refs/heads/master.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.
lean-mcp

Lean MCP

A small MCP server for WordPress. Any plugin can declare a Server that exposes its Abilities as MCP tools. There are no runtime dependencies, and nothing loads until a request reaches an MCP route.

  • Speaks MCP 2026-07-28: stateless, with server/discover, tools, prompts and resources.
  • Also answers clients of 2025-11-25 and 2025-06-18, without sessions.
  • Authenticates with WordPress application passwords over HTTP Basic, or runs as a WP-CLI user over stdio.
  • Requires WordPress 6.9+ (Abilities API in core) and PHP 8.4+.

Declaring a Server

add_action('lean_mcp_init', function () {
    if (!function_exists('lean_mcp_register_server')) {
        return;
    }

    lean_mcp_register_server('my-plugin', [
        'title' => 'My Plugin',                    // Default: the id.
        'version' => '1.0.0',                      // Reported to clients. Default: 1.0.0.
        'instructions' => 'Read before you write.',
        'capability' => 'edit_posts',              // Connection Capability. Default: manage_options.
        'abilities' => ['my-plugin/get-thing'],    // By name, in this order...
        'categories' => ['my-plugin'],             // ...plus whole categories...
        'meta' => ['mcp' => ['public' => true]],   // ...plus a meta match...
        'public' => true,                          // ...plus every Ability public for MCP.
        'gateway' => false,                        // List discover/describe/call instead of every Tool.
        'confirm_destructive' => false,            // Ask the user before destructive calls.
        'cache_ttl_ms' => 300000,                  // How long clients may cache lists and resource reads.
        'cache_scope' => 'private',                // Who may share that cache: private or public.
    ]);
});

The Server is then at /wp-json/lean-mcp/my-plugin. Its Tools take the Abilities' names with the slash replaced by a hyphen, so my-plugin/get-thing becomes my-plugin-get-thing. Each Ability keeps its own permission_callback. The Connection Capability only decides who may talk to the Server at all.

An Ability is public for MCP when meta.mcp.public is true, or when meta.public is true and meta.mcp.public is not set.

From an Ability, Lean MCP reads:

  • label → title
  • description
  • input_schema / output_schema. A schema that is not an object gets wrapped in input / result.
  • meta.annotations → MCP hints: readonly, destructive, idempotent, or their MCP names (readOnlyHint…), plus openWorldHint and title
  • meta.mcp.icons
  • meta.mcp._meta
  • meta.mcp.type: tool (the default), prompt or resource

These are the keys mcp-adapter reads, so Abilities written for it carry over.

Tool results

An Ability's result becomes structured content, mirrored as JSON text. A few shapes become something else:

The Ability returns The client gets
a WP_Error an error result with its message; input errors name the parameter
['success' => false, 'error' => '…'] an error result with that message
['type' => 'image', 'data' => <Base64>, 'mimeType' => …], or results with the raw bytes an image block (PNG unless it says otherwise)
['type' => 'resource', 'uri' => …, 'text' or 'blob' => …, 'mimeType' => …], flat or inside resource an embedded resource

An exception, or the WP_Error WordPress 7.1+ makes of one, reaches the model only as "The tool failed with an unexpected error." The details go to the PHP error log, and to lean_mcp_after_tool_call in the WP_Error's data (exception, or WordPress's error).

Prompts

An Ability with meta.mcp.type = 'prompt' is listed by prompts/list; clients usually offer it as a command. Its arguments are meta.mcp.arguments (a list of {name, title?, description?, required?}), or the top-level properties of its input schema. Running the Ability fills the prompt in. It may return a string, ['text' => …], ['role' => …, 'content' => …], ['texts' => [...]] or ['messages' => [...]], and a description beside them.

Resources

An Ability with meta.mcp.type = 'resource' and a meta.mcp.uri with a scheme (site://settings) is listed by resources/list and read with resources/read, which runs it with no input. meta.mcp may add mimeType, size and annotations (audience, priority, lastModified). The Ability returns a string (the text), a list of {uri?, text|blob, mimeType?}, or anything else, which is sent as JSON.

Gateway mode

With 'gateway' => true the Server lists three Tools instead of one per Ability: discover-tools (names, titles, descriptions and hints), describe-tool (one Tool's full definition) and call-tool ({name, arguments}). The client loads three short definitions instead of every input schema. Every hook below still runs for the Tool that call-tool reaches.

Confirming destructive calls

With 'confirm_destructive' => true, a call to an Ability annotated destructive asks the user first. The Server answers with an input_required result carrying an elicitation/create form. The call runs only after the user accepts and the client calls again with that answer. The request state ties the answer to the same user, Tool and arguments for 15 minutes. Only 2026-07-28 clients that declare form elicitation can be asked; others run the call as before, behind their own permission prompts. lean_mcp_confirm_tool_call decides per call.

Connecting a client

Create an application password for a user who has the Connection Capability, then:

{
  "mcpServers": {
    "my-site": {
      "type": "http",
      "url": "https://example.com/wp-json/lean-mcp/my-plugin",
      "headers": { "Authorization": "Basic <base64 of user:application-password>" }
    }
  }
}

wp lean-mcp list --user=<login> shows every Server with its URL, its capability, how many Tools, Prompts and Resources it has, and whether it runs in Gateway mode.

Over stdio

A local client can start the Server as a subprocess instead, as a WP-CLI user who holds the Connection Capability; no application password is needed:

{
  "mcpServers": {
    "my-site": {
      "command": "wp",
      "args": ["--path=/path/to/wordpress", "lean-mcp", "serve", "my-plugin", "--user=admin"]
    }
  }
}

Hooks

Hook Type Use
lean_mcp_init action Declare Servers.
lean_mcp_server_abilities filter The Abilities a Server exposes, by name, in order: hide or reorder them, per user if needed. Receives ($abilities, $server).
lean_mcp_tool_call_input filter Change a call's input; WordPress still validates it. Return a WP_Error to stop the call. Receives ($input, $ability, $server).
lean_mcp_before_tool_call filter Return a WP_Error to stop a call. Receives (null, $ability, $input, $server).
lean_mcp_confirm_tool_call filter Whether a call needs the user's confirmation. Receives ($confirm, $ability, $input, $server).
lean_mcp_tool_call_result filter Change what a call returns, for instance to redact it. Runs for failed calls too. Receives ($result, $ability, $input, $server).
lean_mcp_after_tool_call action Runs after every call, failed or stopped ones included, with the filtered result. Receives ($result, $ability, $input, $server, $user), which is what an audit log needs.
lean_mcp_allowed_origins filter Browser origins allowed besides the site's own. Requests that send no Origin header, such as CLI clients, are always allowed.
lean_mcp_default_server_enabled filter Return true to turn on the Default Server, which exposes every Ability public for MCP.
lean_mcp_default_server_args filter The Default Server's arguments, plus its id. Defaults: default, with manage_options and public.

Development

composer install
composer test      # Pest: the Unit suite; WordPress tests are skipped
composer analyse   # PHPStan, level 8
composer format    # Pint, Laravel preset
composer lint      # Pint, check only
composer check     # lint, analyse and test, in one go
composer dist      # the production copy, in dist/lean-mcp/

The WordPress suite runs against a disposable WordPress in wp-env (Docker):

pnpm install
pnpm env:start        # once; pnpm env:stop when done
pnpm test:wordpress

Design decisions live in docs/adr/. The vocabulary is defined in GLOSSARY.md.