Observability & Wrap-up

Monitoring a Multi-Agent System

⏱ 16 min

Advanced WordPress MCP Architecture built a real McpObservabilityHandlerInterface implementation to answer “what’s happening on this server.” A multi-agent system built out of several servers and multi-step coordinator chains needs a sharper version of the same question: which agent, running which chain, on which step, and how long is each step taking. Be direct about what this lesson is and isn’t: there’s no built-in WordPress orchestration dashboard, no dedicated multi-agent monitoring product. This is the same custom pattern from earlier courses, extended with a bit more structure, because that’s genuinely the honest state of the art here.

What you'll learn in this lesson
Why per-server observability alone isn't enough
A coordinator chain spans multiple ability calls that a per-request metric doesn't naturally group together.
Tagging each step of a chain with a shared chain ID
So a coordinator's four internal ability calls read back as one traceable unit, not four unrelated events.
Timing individual chain steps, not just the whole request
Finding which specific step in a chain is actually slow.
Extending, not replacing, the handler from Course 4/8
One handler class, reused across every server this course has registered.
Prerequisites

The custom McpObservabilityHandlerInterface implementation from Advanced WordPress MCP Architecture’s observability lesson, and the coordinator chain from Lesson 5 of this course. This lesson extends that handler rather than building a new one from scratch.

Step 1: What a per-server handler already gives you, and where it stops

The observability handler from earlier courses answers questions scoped to one server: call volume, error rate by FailureReason, latency per event. Register that same handler class on both the order-ops-support-server and order-ops-fulfillment-server from Lesson 1, and you already get independent metrics per agent role, which server is getting hammered, which one is erroring more. What it doesn’t give you for free is chain-level visibility: when order-ops/coordinate-return calls four other abilities internally via wp_get_ability()->execute(), none of that happens through the adapter’s own request lifecycle, since those calls never go through MCP transport at all, they’re plain PHP function calls. The adapter’s record_event() never fires for them unless you call it yourself.

Step 2: Recording chain-internal events explicitly

The fix is direct: call your own handler’s record_event() method manually from inside the coordinator, the same method the adapter calls automatically for its own request lifecycle, tagged with a chain ID that ties every step together.

// File: order-ops/class-coordinate-return.php (instrumented)
function order_ops_coordinate_return( $input ) {
	$order_id = $input['order_id'];
	$chain_id = wp_generate_password( 12, false ); // one ID shared by every step in this run
	$handler  = new My_Plugin_Observability_Handler();

	$run_step = function ( string $step_name, string $ability_name, array $args ) use ( $chain_id, $handler ) {
		$start  = microtime( true );
		$result = wp_get_ability( $ability_name )->execute( $args );
		$ms     = ( microtime( true ) - $start ) * 1000;

		$handler->record_event(
			is_wp_error( $result ) ? 'chain_step_failure' : 'chain_step_success',
			array(
				'chain'   => 'coordinate-return',
				'chain_id' => $chain_id,
				'step'    => $step_name,
				'ability' => $ability_name,
			),
			$ms
		);

		return $result;
	};

	$eligibility = $run_step( 'validate', 'order-ops/validate-return-eligibility', array( 'order_id' => $order_id ) );
	if ( is_wp_error( $eligibility ) || empty( $eligibility['eligible'] ) ) {
		return new WP_Error( 'not_eligible', 'Order is not eligible for return.' );
	}

	$refund = $run_step( 'refund', 'order-ops/refund-order', array( 'order_id' => $order_id, 'amount' => $eligibility['refund_amount'] ) );
	if ( is_wp_error( $refund ) ) {
		return $refund;
	}

	$restock = $run_step( 'restock', 'order-ops/restock-returned-item', array( 'order_id' => $order_id ) );
	if ( is_wp_error( $restock ) ) {
		return $restock;
	}

	$run_step( 'notify', 'order-ops/notify-customer-refunded', array( 'order_id' => $order_id, 'amount' => $eligibility['refund_amount'] ) );

	return array( 'order_id' => $order_id, 'chain_id' => $chain_id );
}

Every step now records its own chain_step_success or chain_step_failure event, tagged with the same chain_id, the specific step name, and its own duration. Nothing about the adapter’s own request-level metrics changed, this is purely additional, manual instrumentation for the part of the system the adapter genuinely can’t see: work happening inside one execute_callback, not across separate MCP requests.

Step 3: Extending the handler to bucket by chain step

The handler class itself needs almost no change, just recognize the new tags:

// File: includes/class-my-plugin-observability-handler.php (extended)
public function record_event( string $event, array $tags = array(), ?float $duration_ms = null ): void {
	$bucket = 'my_plugin_mcp_metrics_' . gmdate( 'YmdH' );
	$data   = get_transient( $bucket ) ?: array( 'events' => array(), 'by_chain_step' => array() );

	$data['events'][ $event ] = ( $data['events'][ $event ] ?? 0 ) + 1;

	if ( isset( $tags['chain'], $tags['step'] ) ) {
		$key = $tags['chain'] . ':' . $tags['step'];
		$data['by_chain_step'][ $key ]['count'] = ( $data['by_chain_step'][ $key ]['count'] ?? 0 ) + 1;
		if ( null !== $duration_ms ) {
			$data['by_chain_step'][ $key ]['total_ms'] = ( $data['by_chain_step'][ $key ]['total_ms'] ?? 0 ) + $duration_ms;
		}
	}

	set_transient( $bucket, $data, HOUR_IN_SECONDS * 2 );
}

by_chain_step['coordinate-return:restock'] now accumulates a count and total duration specifically for the restock step, across every run of the chain in that hour, exactly what you need to answer “which step in this chain is actually slow” instead of only “how slow was the whole coordinator call.”

What chain-level instrumentation answers that per-server metrics can’t
QuestionPer-server metric aloneWith chain_id and step tags
Which agent server is busiest?YesYes
Which specific step in a chain is slow?No, only total call durationYes, per-step total_ms
Did every step in one specific run complete?NoYes, filter events by chain_id
Which failure reason dominates a given step?Only per-ability, not per-step-in-chainYes, tags carry both

Test it

# File: terminal
wp-env run cli wp eval '
wp_get_ability( "order-ops/coordinate-return" )->execute( array( "order_id" => 4821 ) );
var_dump( get_transient( "my_plugin_mcp_metrics_" . gmdate("YmdH") )["by_chain_step"] );
'

You should see four keys, coordinate-return:validate, coordinate-return:refund, coordinate-return:restock, and coordinate-return:notify, each with a count and total_ms. Run the chain a handful of times and confirm the counts accumulate correctly and that one deliberately slowed-down step (add a usleep() to test) shows a clearly higher average in its bucket than the others.

Don't assume the adapter's own metrics cover internal ability calls

It’s easy to register a real observability handler on your servers, see it working for ordinary tool calls, and assume a coordinator’s internal chain is automatically covered too. It isn’t, wp_get_ability()->execute() calls never pass through the adapter’s request lifecycle, so nothing calls record_event() for them unless your own coordinator code does, as shown in Step 2. This is genuinely custom-pattern territory: there is no built-in orchestration-level monitoring in WordPress or the MCP Adapter to fall back on.

Recap

Per-server observability from earlier courses still matters and still works unchanged, but a coordinator chain’s internal steps happen outside the adapter’s own request lifecycle, so they need explicit, manual record_event() calls tagged with a shared chain ID and a step name. That’s a deliberate extension of the same McpObservabilityHandlerInterface handler you already built, not a new monitoring system, and it’s the only way to see which specific step in a multi-step chain is actually the slow or failure-prone one.

Resources & further reading

← Handling Partial Failures in a Multi-Step Chain Course Recap: When Multi-Agent Complexity Is Worth It →