Architecture

Multi-Server Architectures at Scale

⏱ 16 min

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.

What you'll learn in this lesson
The full create_server() signature
Every parameter, not just the two or three Course 2 covered.
Per-server transport permission callbacks
How to gate an entire server, not just individual abilities, by who can even connect.
Real naming and routing conventions
Avoiding namespace and route collisions across many servers on one site.
Operational patterns for many servers
Listing, serving, and reasoning about a fleet of servers instead of one.
Prerequisites

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.

A three-server layout on one site
Server IDAudienceAbilities exposedTransport permission
mcp-adapter-default-serverWhatever’s marked meta.mcp.public => trueAuto-discoveredis_user_logged_in() (adapter default)
support-readonlyHelpdesk assistant3 read-only ticket/KB abilitiesview_support_tickets capability
partner-apiExternal integration partner2 narrow, versioned abilitiesCustom check against a partner-specific role
The default server is still on unless you disable it

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

← Designing Extensible Custom Integrations Observability and Monitoring for MCP Servers →