Course 2 introduced the idea that the MCP Adapter can run more than one named server.
At scale, that stops being a curiosity and becomes the whole architecture: a large
site, agency, or platform rarely wants one server exposing every ability to every
agent. It wants a support-readonly server for a helpdesk integration, a
content-ops server for an editorial team’s assistant, and a partner-api server
exposing a narrow, stable slice of abilities to an external integration partner, each
with its own abilities, its own transport, and its own permission boundary. This lesson
goes deep on the real API behind that, McpAdapter::create_server(), and the design
decisions that come with running several servers on one install.
This lesson assumes you’ve already registered a default MCP server and at least one
ability (Course 2), and that you’re comfortable with permission_callback per ability
(Course 4). We’re going one level up, from per-ability permissions to per-server
permissions.
Step 1: Every server is created inside mcp_adapter_init
The adapter fires a single action, mcp_adapter_init, passing itself as the argument.
Every custom server, no matter how many you run, gets created inside a callback hooked
to this action. Creating a server outside it is refused: the adapter checks
doing_action( 'mcp_adapter_init' ) internally and returns a WP_Error (via
_doing_it_wrong()) if you call create_server() at the wrong time.
// File: my-plugin.php
add_action( 'mcp_adapter_init', function ( \WP\MCP\Core\McpAdapter $adapter ) {
$adapter->create_server(
'support-readonly', // server_id
'my-plugin/support', // server_route_namespace
'mcp', // server_route
'Support Read-Only Server', // server_name
'Read-only ticket and KB abilities for the helpdesk integration.', // server_description
'v1.0.0', // server_version
array( \WP\MCP\Transport\HttpTransport::class ), // mcp_transports
\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
array( // tools (ability names)
'my-plugin/list-tickets',
'my-plugin/get-ticket',
'my-plugin/search-kb-articles',
),
array(), // resources
array(), // prompts
function () { // transport_permission_callback
return current_user_can( 'view_support_tickets' );
}
);
} );
That final parameter, transport_permission_callback, is the piece most tutorials skip.
If you don’t pass one, the adapter defaults to is_user_logged_in(), meaning any
authenticated user can even open a connection to that server, before any individual
ability’s permission_callback runs. Passing your own callback here gates the entire
server, a much coarser and often more useful control when a server represents an
audience, not a single capability.
Step 2: One site, several servers, no collisions
Each server needs its own unique server_id and its own REST route (server_route_namespace
plus server_route), because the adapter registers a distinct REST endpoint per
server. Collisions here are a real production bug, not a theoretical one, once several
teams are each adding their own create_server() call.
| Server ID | Audience | Abilities exposed | Transport permission |
|---|---|---|---|
mcp-adapter-default-server | Whatever’s marked meta.mcp.public => true | Auto-discovered | is_user_logged_in() (adapter default) |
support-readonly | Helpdesk assistant | 3 read-only ticket/KB abilities | view_support_tickets capability |
partner-api | External integration partner | 2 narrow, versioned abilities | Custom check against a partner-specific role |
create_server() calls for your own servers don’t remove the adapter’s own default
server. If you want a locked-down, servers-only-by-design site, filter
mcp_adapter_create_default_server to return false, then register every ability
explicitly onto one of your named servers instead of relying on meta.mcp.public
auto-discovery.
Step 3: Serving and listing a fleet of servers
Once you have several servers registered, day-to-day operations shift from “is my server running” to “which servers exist, and what’s each one exposing.” WP-CLI covers both:
# See every registered server and its route
wp mcp-adapter list --format=table
# Serve one specific server over STDIO, as a specific user
wp mcp-adapter serve --server=partner-api --user=partner-integration-bot
Running each server as a distinct WordPress user (as in the partner-api example
above) is a deliberate, useful pattern: it means every request that server processes
runs under a real, auditable user account with its own role and capabilities, rather
than an ambient “whatever the process happens to run as” identity.
Step 4: Deciding when a new server is actually warranted
Not every distinction needs a new server. A single server with several abilities, each
gated by its own permission_callback, is simpler to operate and is the right default.
Reach for a new named server when the distinction is about the audience or transport,
not the ability itself: a different STDIO-only internal tooling server versus an
HTTP-only partner-facing one, a server meant to be fully disabled during a maintenance
window without touching unrelated abilities, or a server whose entire tool list needs
to be swappable (blue/green) independently of the rest of the site.
Test it
Register two servers (the default plus one custom one from Step 1), then confirm both show up distinctly:
wp-env run cli wp mcp-adapter list --format=json
You should see two entries with different server_id and server_route values.
Connect an MCP client to each route in turn and confirm the tool list returned by
tools/list differs between them, matching the tools array you passed to each
create_server() call.
Recap
create_server() takes a server ID, its own route, its own transports, its own error
and observability handlers, its own explicit ability list, and, critically, its own
transport_permission_callback for gating who can connect at all. Multiple servers on
one site is a real, supported pattern, not a workaround, and the right axis to split on
is audience and transport, not just “these abilities feel related.”
Resources & further reading
- MCP Adapter repository, GitHub
- MCP Adapter announcement: from abilities to AI agents, developer.wordpress.org
- Model Context Protocol specification, modelcontextprotocol.io