Course 3 ended with one working assistant: a single MCP server, a handful of abilities, one agent that could plan a multi-step task on its own. It’s tempting to keep growing that one agent forever, adding abilities as new needs come up, until it can do almost anything on your site. This lesson argues that’s usually the wrong instinct once a site has genuinely different audiences to serve, and shows what “more than one agent” actually looks like when you build it out of the primitives you already have.
Course 3 completed, so you’re comfortable with a connected client, permission callbacks,
and multi-step tool chaining. Familiarity with McpAdapter::create_server() from Course 2
or Course 8 helps, since Step 3 reuses it directly, but this lesson explains enough of it
to follow along either way.
Step 1: What “multi-agent” means here, honestly
There is no WordPress “Multi-Agent API.” What this course calls an agent is just: one MCP
server, exposing one deliberately scoped set of abilities, typically connected to by one
kind of client or user. Two agents means two servers, each with its own ability list and
its own transport permission callback, both registered inside the same mcp_adapter_init
callback, the exact mechanism Course 2 introduced and Course 8 went deep on. Nothing new is
being invented in this course, the pattern is entirely in how you choose to split abilities
across servers and design the abilities that let those servers coordinate.
Step 2: What one generalist agent quietly costs you
A single server exposing every ability on your site to every client works fine at first. It gets worse in three specific, compounding ways as the ability list grows.
| Problem | Why it happens |
|---|---|
| Wide permission scope | One transport permission callback has to be loose enough to admit every legitimate caller, a customer-facing chat widget and an internal admin tool end up sharing the same bar for entry. |
| Muddier ability descriptions | A model choosing among thirty abilities from every part of the site has far less context to disambiguate two similarly named tools than a model choosing among five abilities that all belong to one clear role. |
| Harder debugging | When something goes wrong, “which agent, doing what, called which ability” is genuinely unclear if every caller shares one server and one ability list, you’re reading one merged log instead of one log per role. |
None of these are fatal on a small site with two or three abilities. They become real operational pain once an ability list grows past a dozen entries serving more than one kind of caller, which is exactly the point where teams reach for this course’s patterns.
Step 3: A server boundary as an agent boundary
The running example for the rest of this course is a small support-and-fulfillment system. A customer-facing chat agent should be able to look up an order’s status and open an escalation. An internal fulfillment agent, used only by staff tooling, should be able to actually process a return or reship an order. Those are two different trust levels and two different tool sets, so they become two servers:
// File: order-ops.php
add_action( 'mcp_adapter_init', function ( $adapter ) {
// Customer-facing: narrow, read-mostly, safe for a public chat widget.
$adapter->create_server(
'order-ops-support-server',
'order-ops/support',
'mcp',
'Order Ops Support Server',
'Read-only order lookup and escalation-opening abilities for customer chat.',
'v1.0.0',
array( \WP\MCP\Transport\HttpTransport::class ),
\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
array(
'order-ops/get-order-status',
'order-ops/open-escalation',
),
array(),
array(),
function () {
return true; // gated by your own API key or session check, not staff capabilities
}
);
// Internal-only: the abilities that actually change fulfillment state.
$adapter->create_server(
'order-ops-fulfillment-server',
'order-ops/fulfillment',
'mcp',
'Order Ops Fulfillment Server',
'Staff-only abilities for processing returns and reshipping orders.',
'v1.0.0',
array( \WP\MCP\Transport\HttpTransport::class ),
\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
array(
'order-ops/process-return',
'order-ops/reship-order',
),
array(),
array(),
function () {
return current_user_can( 'manage_woocommerce' ) || current_user_can( 'edit_shop_orders' );
}
);
} );
The support agent’s tool list has exactly two entries in it. It cannot call
order-ops/reship-order even if a compromised or careless client tried, that ability isn’t
on this server at all. The fulfillment agent’s tool list is equally narrow in the other
direction. Each server’s permission callback only has to answer one question for one
audience, not a compromise question for both at once.
Step 4: Narrower tool sets make descriptions do less work
With only two abilities on the support server, order-ops/get-order-status’s description
doesn’t need to explain how it differs from five other order-related tools, there aren’t
five others in view. Compare the mental load a model faces:
Generalist server (30 abilities): "is this the order lookup for staff, or for
customers, or the one that also triggers a refund?"
Support server (2 abilities): "there are two tools here, one reads status,
one opens an escalation. The task is asking about status."
This isn’t a claim that models can’t handle large tool lists, capable clients often manage just fine. It’s that a smaller, role-scoped list removes an entire class of ambiguity before the model ever has to resolve it, and removes it structurally rather than through a better-written description.
Test it
Register both servers from Step 3, then confirm the split actually holds:
# File: terminal
wp mcp-adapter list --format=table
Connect a client to order-ops-support-server and confirm tools/list returns only the
two support abilities. Then connect as a staff user to order-ops-fulfillment-server and
confirm it returns only the two fulfillment abilities, and that connecting as a
non-privileged user to that second server is rejected outright by its
transport_permission_callback, not merely by an individual ability’s own check.
Two servers means two things to register, two permission callbacks to keep correct, and two places to look when debugging a caller’s behavior. That tradeoff is worth it once a generalist agent’s tool list has genuinely mixed audiences or trust levels. It is not automatically worth it for every site, and the final lesson in this course comes back to exactly where that line sits.
Recap
A “specialized agent” in this course is just an MCP server with a narrow, role-scoped
ability list and its own transport permission callback, both built with the same
McpAdapter::create_server() call Course 2 and Course 8 already covered. Splitting a
growing generalist server into role-scoped ones tightens permission scope, sharpens
ability descriptions by shrinking the set a model has to disambiguate, and makes debugging
a specific role’s behavior easier since its calls aren’t mixed with every other caller’s.
The support-and-fulfillment example from Step 3 carries through the rest of this course.
Resources & further reading
- MCP Adapter repository, GitHub
- Abilities API reference, developer.wordpress.org
- Model Context Protocol specification, modelcontextprotocol.io